* [PATCH bpf-next v4 0/3] bpf, arena: fix range_tree consistency on allocation failure
@ 2026-08-24 13:40 chenyuan_fl
2026-08-24 13:40 ` [PATCH bpf-next v4 1/3] bpf, arena: fix range_tree_clear inconsistency on kmalloc_nolock failure chenyuan_fl
` (2 more replies)
0 siblings, 3 replies; 15+ messages in thread
From: chenyuan_fl @ 2026-08-24 13:40 UTC (permalink / raw)
To: bpf
Cc: linux-kernel, Alexei Starovoitov, Daniel Borkmann,
Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
Emil Tsalapatis, Ihor Solodrai, Yuan Chen
From: Yuan Chen <chenyuan@kylinos.cn>
The arena range tree can be left inconsistent when kmalloc_nolock()
fails mid-operation. Patch 1 fixes range_tree_clear(), patch 2 fixes
range_tree_set(), patch 3 makes the arena free paths handle
range_tree_set() failures and checks the return value in
arena_alloc_pages()'s partial-allocation error path.
Changes in v4:
- arena_free_worker(): keep a span whose range_tree_set() failed on
arena->free_spans and retry it on a later worker run, instead of
leaving it in the drained list where the second loop would still
zap user VMAs and free the span (dropping the free request), as
pointed out by Emil Tsalapatis.
Changes in v3:
- Check range_tree_set() return value in arena_alloc_pages()'s error
path, which restores the unpopulated tail of a partially allocated
range (previously ignored), as pointed out in review.
Changes in v2:
- Fix multi-line comment style in patches 1 and 3 (opening /* on its
own line), as pointed out in review.
Note: arena_vm_fault()'s two recovery paths (restoring the range to the
free tree after allocation/mapping failure) also call range_tree_set()
without checking the return value; that is addressed in a separate
series.
Yuan Chen (3):
bpf, arena: fix range_tree_clear inconsistency on kmalloc_nolock
failure
bpf, arena: fix range_tree_set inconsistency on kmalloc_nolock failure
bpf, arena: check range_tree_set return in arena_free_pages and
arena_free_worker
kernel/bpf/arena.c | 43 ++++++++++++++++++++++++-----
kernel/bpf/range_tree.c | 61 ++++++++++++++++++++++++++++++-----------
2 files changed, 81 insertions(+), 23 deletions(-)
--
2.54.0
^ permalink raw reply [flat|nested] 15+ messages in thread* [PATCH bpf-next v4 1/3] bpf, arena: fix range_tree_clear inconsistency on kmalloc_nolock failure 2026-08-24 13:40 [PATCH bpf-next v4 0/3] bpf, arena: fix range_tree consistency on allocation failure chenyuan_fl @ 2026-08-24 13:40 ` chenyuan_fl 2026-08-24 14:35 ` bot+bpf-ci 2026-08-24 13:40 ` [PATCH bpf-next v4 2/3] bpf, arena: fix range_tree_set " chenyuan_fl 2026-08-24 13:40 ` [PATCH bpf-next v4 3/3] bpf, arena: check range_tree_set return in arena_free_pages and arena_free_worker chenyuan_fl 2 siblings, 1 reply; 15+ messages in thread From: chenyuan_fl @ 2026-08-24 13:40 UTC (permalink / raw) To: bpf Cc: linux-kernel, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis, Ihor Solodrai, Yuan Chen From: Yuan Chen <chenyuan@kylinos.cn> range_tree_clear() pre-allocates the right-half node before modifying the tree, so an allocation failure returns -ENOMEM without altering the range tree. Signed-off-by: Yuan Chen <chenyuan@kylinos.cn> --- kernel/bpf/range_tree.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/kernel/bpf/range_tree.c b/kernel/bpf/range_tree.c index 2f28886f3ff7..15b588377a76 100644 --- a/kernel/bpf/range_tree.c +++ b/kernel/bpf/range_tree.c @@ -143,16 +143,22 @@ int range_tree_clear(struct range_tree *rt, u32 start, u32 len) if (rn->rn_start < start && rn->rn_last > last) { u32 old_last = rn->rn_last; + /* + * Pre-allocate the right-half node before modifying + * the tree. If allocation fails we return -ENOMEM + * without altering the range tree. + */ + new_rn = kmalloc_nolock(sizeof(struct range_node), + __GFP_ACCOUNT, NUMA_NO_NODE); + if (!new_rn) + return -ENOMEM; + /* Overlaps with the entire clearing range */ range_it_remove(rn, rt); rn->rn_last = start - 1; range_it_insert(rn, rt); - /* Add a range */ - new_rn = kmalloc_nolock(sizeof(struct range_node), __GFP_ACCOUNT, - NUMA_NO_NODE); - if (!new_rn) - return -ENOMEM; + /* Add right-half range */ new_rn->rn_start = last + 1; new_rn->rn_last = old_last; range_it_insert(new_rn, rt); -- 2.54.0 ^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH bpf-next v4 1/3] bpf, arena: fix range_tree_clear inconsistency on kmalloc_nolock failure 2026-08-24 13:40 ` [PATCH bpf-next v4 1/3] bpf, arena: fix range_tree_clear inconsistency on kmalloc_nolock failure chenyuan_fl @ 2026-08-24 14:35 ` bot+bpf-ci 0 siblings, 0 replies; 15+ messages in thread From: bot+bpf-ci @ 2026-08-24 14:35 UTC (permalink / raw) To: chenyuan_fl, bpf Cc: linux-kernel, ast, daniel, andrii, eddyz87, memxor, martin.lau, song, yonghong.song, jolsa, emil, ihor.solodrai, chenyuan, ast, andrii, daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai [-- Attachment #1: Type: text/plain, Size: 1599 bytes --] > diff --git a/kernel/bpf/range_tree.c b/kernel/bpf/range_tree.c > index 2f28886f3ff7..15b588377a76 100644 > --- a/kernel/bpf/range_tree.c > +++ b/kernel/bpf/range_tree.c > @@ -143,16 +143,22 @@ int range_tree_clear(struct range_tree *rt, u32 start, u32 len) > if (rn->rn_start < start && rn->rn_last > last) { > u32 old_last = rn->rn_last; > > + /* > + * Pre-allocate the right-half node before modifying > + * the tree. If allocation fails we return -ENOMEM > + * without altering the range tree. > + */ > + new_rn = kmalloc_nolock(sizeof(struct range_node), > + __GFP_ACCOUNT, NUMA_NO_NODE); > + if (!new_rn) > + return -ENOMEM; > + > /* Overlaps with the entire clearing range */ > range_it_remove(rn, rt); > rn->rn_last = start - 1; > range_it_insert(rn, rt); > > - /* Add a range */ > - new_rn = kmalloc_nolock(sizeof(struct range_node), __GFP_ACCOUNT, > - NUMA_NO_NODE); > - if (!new_rn) > - return -ENOMEM; > + /* Add right-half range */ > new_rn->rn_start = last + 1; > new_rn->rn_last = old_last; > range_it_insert(new_rn, rt); This isn't a bug, but would it read better to keep the "Overlaps with the entire clearing range" comment at the top of the branch, next to the condition it describes, rather than having it appear in the middle after the new allocation block? --- 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/32736729475 ^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH bpf-next v4 2/3] bpf, arena: fix range_tree_set inconsistency on kmalloc_nolock failure 2026-08-24 13:40 [PATCH bpf-next v4 0/3] bpf, arena: fix range_tree consistency on allocation failure chenyuan_fl 2026-08-24 13:40 ` [PATCH bpf-next v4 1/3] bpf, arena: fix range_tree_clear inconsistency on kmalloc_nolock failure chenyuan_fl @ 2026-08-24 13:40 ` chenyuan_fl 2026-08-24 14:35 ` bot+bpf-ci 2026-08-27 2:56 ` Alexei Starovoitov 2026-08-24 13:40 ` [PATCH bpf-next v4 3/3] bpf, arena: check range_tree_set return in arena_free_pages and arena_free_worker chenyuan_fl 2 siblings, 2 replies; 15+ messages in thread From: chenyuan_fl @ 2026-08-24 13:40 UTC (permalink / raw) To: bpf Cc: linux-kernel, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis, Ihor Solodrai, Yuan Chen From: Yuan Chen <chenyuan@kylinos.cn> range_tree_set() pre-allocates the node needed for a brand-new range before calling range_tree_clear(), so an allocation failure returns -ENOMEM without having modified the tree (previously the overlapping nodes were already removed by range_tree_clear() before the allocation was attempted, permanently losing the cleared sub-ranges). Signed-off-by: Yuan Chen <chenyuan@kylinos.cn> --- kernel/bpf/range_tree.c | 45 +++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/kernel/bpf/range_tree.c b/kernel/bpf/range_tree.c index 15b588377a76..54055b1fe541 100644 --- a/kernel/bpf/range_tree.c +++ b/kernel/bpf/range_tree.c @@ -199,6 +199,7 @@ int is_range_tree_set(struct range_tree *rt, u32 start, u32 len) int range_tree_set(struct range_tree *rt, u32 start, u32 len) { u32 last = start + len - 1; + struct range_node *new_rn = NULL; struct range_node *right; struct range_node *left; int err; @@ -208,20 +209,40 @@ int range_tree_set(struct range_tree *rt, u32 start, u32 len) if (left && left->rn_start <= start && left->rn_last >= last) return 0; + /* + * A new node is needed only when the range has no adjacent free + * range on either side. This is known before clearing: any range + * covering start - 1 or last + 1 survives the clear as an adjacent + * piece. Allocate only in that case, before modifying the tree, so + * a failure leaves the range tree unmodified + */ + left = range_it_iter_first(rt, start - 1, start - 1); + right = range_it_iter_first(rt, last + 1, last + 1); + if (!left && !right) { + new_rn = kmalloc_nolock(sizeof(struct range_node), + __GFP_ACCOUNT, NUMA_NO_NODE); + if (!new_rn) + return -ENOMEM; + } + /* Clear out everything in the range we want to set. */ err = range_tree_clear(rt, start, len); if (err) - return err; + goto out_free_new; /* Do we have a left-adjacent range ? */ left = range_it_iter_first(rt, start - 1, start - 1); - if (left && left->rn_last + 1 != start) - return -EFAULT; + if (left && left->rn_last + 1 != start) { + err = -EFAULT; + goto out_free_new; + } /* Do we have a right-adjacent range ? */ right = range_it_iter_first(rt, last + 1, last + 1); - if (right && right->rn_start != last + 1) - return -EFAULT; + if (right && right->rn_start != last + 1) { + err = -EFAULT; + goto out_free_new; + } if (left && right) { /* Combine left and right adjacent ranges */ @@ -241,14 +262,16 @@ int range_tree_set(struct range_tree *rt, u32 start, u32 len) right->rn_start = start; range_it_insert(right, rt); } else { - left = kmalloc_nolock(sizeof(struct range_node), __GFP_ACCOUNT, NUMA_NO_NODE); - if (!left) - return -ENOMEM; - left->rn_start = start; - left->rn_last = last; - range_it_insert(left, rt); + /* No adjacent ranges; use the pre-allocated node */ + new_rn->rn_start = start; + new_rn->rn_last = last; + range_it_insert(new_rn, rt); } return 0; + +out_free_new: + kfree_nolock(new_rn); + return err; } void range_tree_destroy(struct range_tree *rt) -- 2.54.0 ^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH bpf-next v4 2/3] bpf, arena: fix range_tree_set inconsistency on kmalloc_nolock failure 2026-08-24 13:40 ` [PATCH bpf-next v4 2/3] bpf, arena: fix range_tree_set " chenyuan_fl @ 2026-08-24 14:35 ` bot+bpf-ci 2026-08-27 2:56 ` Alexei Starovoitov 1 sibling, 0 replies; 15+ messages in thread From: bot+bpf-ci @ 2026-08-24 14:35 UTC (permalink / raw) To: chenyuan_fl, bpf Cc: linux-kernel, ast, daniel, andrii, eddyz87, memxor, martin.lau, song, yonghong.song, jolsa, emil, ihor.solodrai, chenyuan, ast, andrii, daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai [-- Attachment #1: Type: text/plain, Size: 1422 bytes --] > bpf, arena: fix range_tree_set inconsistency on kmalloc_nolock failure > > range_tree_set() pre-allocates the node needed for a brand-new range > before calling range_tree_clear(), so an allocation failure returns > -ENOMEM without having modified the tree (previously the overlapping > nodes were already removed by range_tree_clear() before the allocation > was attempted, permanently losing the cleared sub-ranges). > > Signed-off-by: Yuan Chen <chenyuan@kylinos.cn> This isn't a bug, but would it be worth adding a Fixes: tag for the commit that introduced the clear-then-allocate ordering, so the stable backport scope is clear? The introducing commit could not be determined from this checkout. Also, could the body be split into two sentences so the "previously ..." rationale is not buried in a parenthetical? For example: range_tree_set() pre-allocates the node needed for a brand-new range before calling range_tree_clear(), so an allocation failure returns -ENOMEM without having modified the tree. Previously the overlapping nodes were already removed by range_tree_clear() before the allocation was attempted, permanently losing the cleared sub-ranges. --- 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/32736729475 ^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH bpf-next v4 2/3] bpf, arena: fix range_tree_set inconsistency on kmalloc_nolock failure 2026-08-24 13:40 ` [PATCH bpf-next v4 2/3] bpf, arena: fix range_tree_set " chenyuan_fl 2026-08-24 14:35 ` bot+bpf-ci @ 2026-08-27 2:56 ` Alexei Starovoitov 2026-09-01 7:01 ` chenyuan 2026-09-02 9:37 ` [PATCH bpf-next v5 0/3] bpf, arena: fix range_tree consistency on allocation failure chenyuan_fl 1 sibling, 2 replies; 15+ messages in thread From: Alexei Starovoitov @ 2026-08-27 2:56 UTC (permalink / raw) To: Yuan Chen Cc: bpf, LKML, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis, Ihor Solodrai, Yuan Chen On Mon, Aug 24, 2026 at 6:40 AM <chenyuan_fl@163.com> wrote: > > From: Yuan Chen <chenyuan@kylinos.cn> > > range_tree_set() pre-allocates the node needed for a brand-new range > before calling range_tree_clear(), so an allocation failure returns > -ENOMEM without having modified the tree (previously the overlapping > nodes were already removed by range_tree_clear() before the allocation > was attempted, permanently losing the cleared sub-ranges). > > Signed-off-by: Yuan Chen <chenyuan@kylinos.cn> > --- > kernel/bpf/range_tree.c | 45 +++++++++++++++++++++++++++++++---------- > 1 file changed, 34 insertions(+), 11 deletions(-) > > diff --git a/kernel/bpf/range_tree.c b/kernel/bpf/range_tree.c > index 15b588377a76..54055b1fe541 100644 > --- a/kernel/bpf/range_tree.c > +++ b/kernel/bpf/range_tree.c > @@ -199,6 +199,7 @@ int is_range_tree_set(struct range_tree *rt, u32 start, u32 len) > int range_tree_set(struct range_tree *rt, u32 start, u32 len) > { > u32 last = start + len - 1; > + struct range_node *new_rn = NULL; > struct range_node *right; > struct range_node *left; > int err; > @@ -208,20 +209,40 @@ int range_tree_set(struct range_tree *rt, u32 start, u32 len) > if (left && left->rn_start <= start && left->rn_last >= last) > return 0; > > + /* > + * A new node is needed only when the range has no adjacent free > + * range on either side. This is known before clearing: any range > + * covering start - 1 or last + 1 survives the clear as an adjacent > + * piece. If this is true, why do a 2nd call to left = range_it_iter_first() ? > Allocate only in that case, before modifying the tree, so > + * a failure leaves the range tree unmodified > + */ > + left = range_it_iter_first(rt, start - 1, start - 1); > + right = range_it_iter_first(rt, last + 1, last + 1); > + if (!left && !right) { > + new_rn = kmalloc_nolock(sizeof(struct range_node), > + __GFP_ACCOUNT, NUMA_NO_NODE); > + if (!new_rn) > + return -ENOMEM; > + } > + > /* Clear out everything in the range we want to set. */ > err = range_tree_clear(rt, start, len); > if (err) > - return err; > + goto out_free_new; > > /* Do we have a left-adjacent range ? */ > left = range_it_iter_first(rt, start - 1, start - 1); pw-bot: cr ^ permalink raw reply [flat|nested] 15+ messages in thread
* Re:Re: [PATCH bpf-next v4 2/3] bpf, arena: fix range_tree_set inconsistency on kmalloc_nolock failure 2026-08-27 2:56 ` Alexei Starovoitov @ 2026-09-01 7:01 ` chenyuan 2026-09-02 9:37 ` [PATCH bpf-next v5 0/3] bpf, arena: fix range_tree consistency on allocation failure chenyuan_fl 1 sibling, 0 replies; 15+ messages in thread From: chenyuan @ 2026-09-01 7:01 UTC (permalink / raw) To: Alexei Starovoitov Cc: bpf, LKML, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis, Ihor Solodrai, Yuan Chen The two lookups answer different questions, so the second one is not redundant: - The pre-clear lookup only consumes the boolean result. Nodes are disjoint, and a node covering both start - 1 and last + 1 would fully cover [start, last], which is rejected by the early return above. Hence range_tree_clear() can only remove or truncate nodes overlapping [start, last]: a node covering start - 1 either ends there (untouched) or straddles start and is truncated to [rn_start, start - 1]. Adjacency on either side is therefore invariant across the clear, and "no adjacent node on either side" before the clear is exactly the condition for the else-branch -- the only case needing a fresh node. It must be evaluated before any tree modification to keep the -ENOMEM path side-effect free. - The post-clear lookup fetches the node handles used by the merge/extend branches. The pre-clear handles cannot be reused: an adjacent node may straddle the range and get truncated (e.g. [start - 1, start + 3] becomes [start - 1, start - 1]), so both its bounds and its position in the tree change. Re-looking it up keeps range_tree_set() independent of how range_tree_clear() implements truncation, and leaves the -EFAULT checks below as a sanity check of the clear itself. The comment indeed fails to spell this out (and "adjacent free range" is backwards); I'll reword it in v5. At 2026-08-27 10:56:04, "Alexei Starovoitov" <alexei.starovoitov@gmail.com> wrote: >On Mon, Aug 24, 2026 at 6:40 AM <chenyuan_fl@163.com> wrote: >> >> From: Yuan Chen <chenyuan@kylinos.cn> >> >> range_tree_set() pre-allocates the node needed for a brand-new range >> before calling range_tree_clear(), so an allocation failure returns >> -ENOMEM without having modified the tree (previously the overlapping >> nodes were already removed by range_tree_clear() before the allocation >> was attempted, permanently losing the cleared sub-ranges). >> >> Signed-off-by: Yuan Chen <chenyuan@kylinos.cn> >> --- >> kernel/bpf/range_tree.c | 45 +++++++++++++++++++++++++++++++---------- >> 1 file changed, 34 insertions(+), 11 deletions(-) >> >> diff --git a/kernel/bpf/range_tree.c b/kernel/bpf/range_tree.c >> index 15b588377a76..54055b1fe541 100644 >> --- a/kernel/bpf/range_tree.c >> +++ b/kernel/bpf/range_tree.c >> @@ -199,6 +199,7 @@ int is_range_tree_set(struct range_tree *rt, u32 start, u32 len) >> int range_tree_set(struct range_tree *rt, u32 start, u32 len) >> { >> u32 last = start + len - 1; >> + struct range_node *new_rn = NULL; >> struct range_node *right; >> struct range_node *left; >> int err; >> @@ -208,20 +209,40 @@ int range_tree_set(struct range_tree *rt, u32 start, u32 len) >> if (left && left->rn_start <= start && left->rn_last >= last) >> return 0; >> >> + /* >> + * A new node is needed only when the range has no adjacent free >> + * range on either side. This is known before clearing: any range >> + * covering start - 1 or last + 1 survives the clear as an adjacent >> + * piece. > >If this is true, why do a 2nd call to left = range_it_iter_first() ? > > >> Allocate only in that case, before modifying the tree, so >> + * a failure leaves the range tree unmodified >> + */ >> + left = range_it_iter_first(rt, start - 1, start - 1); >> + right = range_it_iter_first(rt, last + 1, last + 1); >> + if (!left && !right) { >> + new_rn = kmalloc_nolock(sizeof(struct range_node), >> + __GFP_ACCOUNT, NUMA_NO_NODE); >> + if (!new_rn) >> + return -ENOMEM; >> + } >> + >> /* Clear out everything in the range we want to set. */ >> err = range_tree_clear(rt, start, len); >> if (err) >> - return err; >> + goto out_free_new; >> >> /* Do we have a left-adjacent range ? */ >> left = range_it_iter_first(rt, start - 1, start - 1); > >pw-bot: cr ^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH bpf-next v5 0/3] bpf, arena: fix range_tree consistency on allocation failure 2026-08-27 2:56 ` Alexei Starovoitov 2026-09-01 7:01 ` chenyuan @ 2026-09-02 9:37 ` chenyuan_fl 2026-09-02 9:37 ` [PATCH bpf-next v5 1/3] bpf, arena: fix range_tree_clear inconsistency on kmalloc_nolock failure chenyuan_fl ` (2 more replies) 1 sibling, 3 replies; 15+ messages in thread From: chenyuan_fl @ 2026-09-02 9:37 UTC (permalink / raw) To: bpf Cc: linux-kernel, Alexei Starovoitov, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis, Ihor Solodrai, Yuan Chen From: Yuan Chen <chenyuan@kylinos.cn> The arena range tree can be left inconsistent when kmalloc_nolock() fails mid-operation. Patch 1 fixes range_tree_clear(), patch 2 fixes range_tree_set(), patch 3 makes the arena free paths handle range_tree_set() failures, checks the return value in arena_alloc_pages()'s partial-allocation error path, and makes arena_map_free() drain the deferred-free machinery safely now that the worker can requeue failed spans and queue irq_work on retry. Changes in v5: - arena_map_free(): set a dying flag and steal orphaned spans before draining, and drain with flush_work() + irq_work_sync() + flush_work(). The worker retry queues arena->free_irq, which the old irq_work_sync() + flush_work() order could miss: the irq_work fired after the arena was freed and its callback scheduled free_work on freed memory. - arena_map_free(): retry the spinlock acquisition a bounded number of times (-EDEADLK is not retried) and WARN with the error code, instead of a bare WARN_ON_ONCE(1) and an immediate leak of the arena. - range_tree_set(): reword the comment describing the two lookups, as suggested by Alexei Starovoitov. The pre-clear probe only decides whether a fresh node must be allocated, so that -ENOMEM leaves the tree unmodified; the post-clear lookup fetches the merge handles without depending on how range_tree_clear() truncates overlapping nodes. Changes in v4: - arena_free_worker(): keep a span whose range_tree_set() failed on arena->free_spans and retry it on a later worker run, instead of leaving it in the drained list where the second loop would still zap user VMAs and free the span (dropping the free request), as pointed out by Emil Tsalapatis. Changes in v3: - Check range_tree_set() return value in arena_alloc_pages()'s error path, which restores the unpopulated tail of a partially allocated range (previously ignored), as pointed out in review. Changes in v2: - Fix multi-line comment style in patches 1 and 3 (opening /* on its own line), as pointed out in review. Note: arena_vm_fault()'s two recovery paths (restoring the range to the free tree after allocation/mapping failure) also call range_tree_set() without checking the return value; that is addressed in a separate series. Yuan Chen (3): bpf, arena: fix range_tree_clear inconsistency on kmalloc_nolock failure bpf, arena: fix range_tree_set inconsistency on kmalloc_nolock failure bpf, arena: handle range_tree_set failures in alloc/free paths kernel/bpf/arena.c | 95 ++++++++++++++++++++++++++++++++++---- kernel/bpf/range_tree.c | 61 +++++++++++++++++------- 2 files changed, 132 insertions(+), 24 deletions(-) -- 2.54.0 ^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH bpf-next v5 1/3] bpf, arena: fix range_tree_clear inconsistency on kmalloc_nolock failure 2026-09-02 9:37 ` [PATCH bpf-next v5 0/3] bpf, arena: fix range_tree consistency on allocation failure chenyuan_fl @ 2026-09-02 9:37 ` chenyuan_fl 2026-09-02 9:37 ` [PATCH bpf-next v5 2/3] bpf, arena: fix range_tree_set " chenyuan_fl 2026-09-02 9:37 ` [PATCH bpf-next v5 3/3] bpf, arena: handle range_tree_set failures in alloc/free paths chenyuan_fl 2 siblings, 0 replies; 15+ messages in thread From: chenyuan_fl @ 2026-09-02 9:37 UTC (permalink / raw) To: bpf Cc: linux-kernel, Alexei Starovoitov, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis, Ihor Solodrai, Yuan Chen From: Yuan Chen <chenyuan@kylinos.cn> range_tree_clear() pre-allocates the right-half node before modifying the tree, so an allocation failure returns -ENOMEM without altering the range tree. Signed-off-by: Yuan Chen <chenyuan@kylinos.cn> --- kernel/bpf/range_tree.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/kernel/bpf/range_tree.c b/kernel/bpf/range_tree.c index 2f28886f3ff7..15b588377a76 100644 --- a/kernel/bpf/range_tree.c +++ b/kernel/bpf/range_tree.c @@ -143,16 +143,22 @@ int range_tree_clear(struct range_tree *rt, u32 start, u32 len) if (rn->rn_start < start && rn->rn_last > last) { u32 old_last = rn->rn_last; + /* + * Pre-allocate the right-half node before modifying + * the tree. If allocation fails we return -ENOMEM + * without altering the range tree. + */ + new_rn = kmalloc_nolock(sizeof(struct range_node), + __GFP_ACCOUNT, NUMA_NO_NODE); + if (!new_rn) + return -ENOMEM; + /* Overlaps with the entire clearing range */ range_it_remove(rn, rt); rn->rn_last = start - 1; range_it_insert(rn, rt); - /* Add a range */ - new_rn = kmalloc_nolock(sizeof(struct range_node), __GFP_ACCOUNT, - NUMA_NO_NODE); - if (!new_rn) - return -ENOMEM; + /* Add right-half range */ new_rn->rn_start = last + 1; new_rn->rn_last = old_last; range_it_insert(new_rn, rt); -- 2.54.0 ^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH bpf-next v5 2/3] bpf, arena: fix range_tree_set inconsistency on kmalloc_nolock failure 2026-09-02 9:37 ` [PATCH bpf-next v5 0/3] bpf, arena: fix range_tree consistency on allocation failure chenyuan_fl 2026-09-02 9:37 ` [PATCH bpf-next v5 1/3] bpf, arena: fix range_tree_clear inconsistency on kmalloc_nolock failure chenyuan_fl @ 2026-09-02 9:37 ` chenyuan_fl 2026-09-02 9:37 ` [PATCH bpf-next v5 3/3] bpf, arena: handle range_tree_set failures in alloc/free paths chenyuan_fl 2 siblings, 0 replies; 15+ messages in thread From: chenyuan_fl @ 2026-09-02 9:37 UTC (permalink / raw) To: bpf Cc: linux-kernel, Alexei Starovoitov, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis, Ihor Solodrai, Yuan Chen From: Yuan Chen <chenyuan@kylinos.cn> range_tree_set() pre-allocates the node needed for a brand-new range before calling range_tree_clear(), so an allocation failure returns -ENOMEM without having modified the tree (previously the overlapping nodes were already removed by range_tree_clear() before the allocation was attempted, permanently losing the cleared sub-ranges). Signed-off-by: Yuan Chen <chenyuan@kylinos.cn> --- kernel/bpf/range_tree.c | 45 +++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/kernel/bpf/range_tree.c b/kernel/bpf/range_tree.c index 15b588377a76..0420ab715f20 100644 --- a/kernel/bpf/range_tree.c +++ b/kernel/bpf/range_tree.c @@ -199,6 +199,7 @@ int is_range_tree_set(struct range_tree *rt, u32 start, u32 len) int range_tree_set(struct range_tree *rt, u32 start, u32 len) { u32 last = start + len - 1; + struct range_node *new_rn = NULL; struct range_node *right; struct range_node *left; int err; @@ -208,20 +209,40 @@ int range_tree_set(struct range_tree *rt, u32 start, u32 len) if (left && left->rn_start <= start && left->rn_last >= last) return 0; + /* + * The pre-clear probe only decides whether a fresh node is needed; + * adjacency on either side is invariant across the clear, so it can + * run before the tree is modified and -ENOMEM leaves it untouched. + * The merge below re-fetches its handles after the clear instead of + * relying on the overlapping nodes being updated in place. + */ + left = range_it_iter_first(rt, start - 1, start - 1); + right = range_it_iter_first(rt, last + 1, last + 1); + if (!left && !right) { + new_rn = kmalloc_nolock(sizeof(struct range_node), + __GFP_ACCOUNT, NUMA_NO_NODE); + if (!new_rn) + return -ENOMEM; + } + /* Clear out everything in the range we want to set. */ err = range_tree_clear(rt, start, len); if (err) - return err; + goto out_free_new; /* Do we have a left-adjacent range ? */ left = range_it_iter_first(rt, start - 1, start - 1); - if (left && left->rn_last + 1 != start) - return -EFAULT; + if (left && left->rn_last + 1 != start) { + err = -EFAULT; + goto out_free_new; + } /* Do we have a right-adjacent range ? */ right = range_it_iter_first(rt, last + 1, last + 1); - if (right && right->rn_start != last + 1) - return -EFAULT; + if (right && right->rn_start != last + 1) { + err = -EFAULT; + goto out_free_new; + } if (left && right) { /* Combine left and right adjacent ranges */ @@ -241,14 +262,16 @@ int range_tree_set(struct range_tree *rt, u32 start, u32 len) right->rn_start = start; range_it_insert(right, rt); } else { - left = kmalloc_nolock(sizeof(struct range_node), __GFP_ACCOUNT, NUMA_NO_NODE); - if (!left) - return -ENOMEM; - left->rn_start = start; - left->rn_last = last; - range_it_insert(left, rt); + /* No adjacent ranges; use the pre-allocated node */ + new_rn->rn_start = start; + new_rn->rn_last = last; + range_it_insert(new_rn, rt); } return 0; + +out_free_new: + kfree_nolock(new_rn); + return err; } void range_tree_destroy(struct range_tree *rt) -- 2.54.0 ^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH bpf-next v5 3/3] bpf, arena: handle range_tree_set failures in alloc/free paths 2026-09-02 9:37 ` [PATCH bpf-next v5 0/3] bpf, arena: fix range_tree consistency on allocation failure chenyuan_fl 2026-09-02 9:37 ` [PATCH bpf-next v5 1/3] bpf, arena: fix range_tree_clear inconsistency on kmalloc_nolock failure chenyuan_fl 2026-09-02 9:37 ` [PATCH bpf-next v5 2/3] bpf, arena: fix range_tree_set " chenyuan_fl @ 2026-09-02 9:37 ` chenyuan_fl 2026-09-02 9:53 ` sashiko-bot 2 siblings, 1 reply; 15+ messages in thread From: chenyuan_fl @ 2026-09-02 9:37 UTC (permalink / raw) To: bpf Cc: linux-kernel, Alexei Starovoitov, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis, Ihor Solodrai, Yuan Chen From: Yuan Chen <chenyuan@kylinos.cn> arena_alloc_pages(), arena_free_pages() and arena_free_worker() now handle range_tree_set() errors. arena_free_pages() aborts the free on error, and arena_free_worker() moves range_tree_set() before PTE clearing so that a failed tree update leaves the PTEs intact instead of freeing pages that the arena free tree does not track. Also check the range_tree_set() return value in arena_alloc_pages()'s error path, which restores the unpopulated tail of a partially allocated range; log a warning instead of silently leaking the virtual range when the tree update fails. range_tree_set() is failure-atomic (it pre-allocates the node before touching the tree), so on -ENOMEM the range stays tracked as allocated and the pages remain mapped and accessible. A failed free is therefore retryable, and arena_map_free() reclaims any retained pages at map destruction; aborting the free avoids clearing PTEs for pages the arena free tree does not track. In arena_free_worker() a failed tree update used to leave the span in the drained list, where the second loop would still flush TLB entries, zap user VMAs, and free the span itself: the free request was dropped, user mappings were destroyed for a free that never happened, and the pages stayed mapped until map destruction. Keep failed spans on arena->free_spans instead and retry them on a later worker run; only spans whose PTE clearing actually ran are flushed, zapped, and released. The retry queues arena->free_irq while the map can concurrently be freed. arena_map_free() relied on irq_work_sync() + flush_work(), which miss an irq_work queued by the running worker between the two calls: the irq_work can fire after the arena is freed and its callback schedules free_work on freed memory. Set arena->dying under the arena spinlock before draining, so the worker stops requeueing, steal the orphaned spans (their pages are reclaimed by existing_page_cb()), and drain with flush_work() + irq_work_sync() + flush_work(). Setting @dying requires the arena spinlock. raw_res_spin_lock_irqsave() can fail (-EDEADLK on a proven deadlock cycle, -ETIMEDOUT after a long hold), and proceeding without the lock would race the worker. Retry a bounded number of times for a long but finite hold and do not retry -EDEADLK; on exhaustion leak the arena with a WARN carrying the error code rather than hang map free. Suggested-by: Emil Tsalapatis <emil@etsalapatis.com> Signed-off-by: Yuan Chen <chenyuan@kylinos.cn> --- kernel/bpf/arena.c | 95 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 8 deletions(-) diff --git a/kernel/bpf/arena.c b/kernel/bpf/arena.c index 7b6847200b43..b0d1f0facfb2 100644 --- a/kernel/bpf/arena.c +++ b/kernel/bpf/arena.c @@ -5,6 +5,7 @@ #include <linux/cacheflush.h> #include <linux/err.h> #include <linux/irq_work.h> +#include <linux/delay.h> #include "linux/filter.h" #include <linux/llist.h> #include <linux/btf_ids.h> @@ -67,6 +68,8 @@ struct bpf_arena { struct irq_work free_irq; struct work_struct free_work; struct llist_head free_spans; + /* set under spinlock during map free; stops the worker retry loop */ + bool dying; }; static void arena_free_worker(struct work_struct *work); @@ -370,6 +373,9 @@ static int existing_page_cb(pte_t *ptep, unsigned long addr, void *data) static void arena_map_free(struct bpf_map *map) { struct bpf_arena *arena = container_of(map, struct bpf_arena, map); + struct llist_node *list, *pos, *t; + unsigned long flags; + int ret, i; /* * Check that user vma-s are not around when bpf map is freed. @@ -380,7 +386,40 @@ static void arena_map_free(struct bpf_map *map) if (WARN_ON_ONCE(!list_empty(&arena->vma_list))) return; - /* Ensure no pending deferred frees */ + /* + * No fallback if this fails, so retry a few times for a long but + * finite hold; -EDEADLK can't be waited out. Cap the retries: + * leaking the arena is better than hanging map free. + */ + for (i = 0; i < 10; i++) { + ret = raw_res_spin_lock_irqsave(&arena->spinlock, flags); + if (!ret || ret == -EDEADLK) + break; + msleep(1); + } + if (ret) { + WARN_ONCE(1, "bpf_arena: spinlock acquire failed %d\n", ret); + return; + } + /* + * Set @dying before draining: the worker checks it under this + * spinlock before requeueing, so a failed span is either stolen + * here or dropped by the worker. + */ + arena->dying = true; + list = llist_del_all(&arena->free_spans); + raw_res_spin_unlock_irqrestore(&arena->spinlock, flags); + + llist_for_each_safe(pos, t, list) + kfree_nolock(llist_entry(pos, struct arena_free_span, node)); + + /* + * flush_work() lets the running worker observe @dying so it stops + * requeueing; irq_work_sync() retires anything queued before that; + * the final flush_work() runs the instance which the retired + * irq_work's callback may have scheduled. + */ + flush_work(&arena->free_work); irq_work_sync(&arena->free_irq); flush_work(&arena->free_work); @@ -766,7 +805,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 +922,20 @@ 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, so -ENOMEM leaves the + * range allocated and the pages mapped; abort the free rather + * than release pages the tree does not track. Nothing retries + * the free; the program can free the range again. + */ + pr_warn_ratelimited("bpf_arena: free of %lx+%ld failed\n", + uaddr, page_cnt); + 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,12 +1031,13 @@ 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; long kaddr, page_cnt, pgoff; unsigned long flags; + bool retry = false; if (raw_res_spin_lock_irqsave(&arena->spinlock, flags)) { schedule_work(work); @@ -992,28 +1047,52 @@ 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); + /* + * Set the range free before clearing PTEs, and requeue the + * span on failure: the PTEs stay intact and the free is + * retried later. Only spans moved to @cleared (PTE clearing + * actually ran) reach the flush/zap/release loop below. + */ + if (range_tree_set(&arena->rt, pgoff, page_cnt)) { + if (arena->dying) { + /* + * The map is being freed. PTEs stay intact + * and the pages are reclaimed by + * arena_map_free() via existing_page_cb(). + */ + kfree_nolock(s); + continue; + } + llist_add(&s->node, &arena->free_spans); + retry = true; + 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); + if (retry) + irq_work_queue(&arena->free_irq); + /* 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 ^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH bpf-next v5 3/3] bpf, arena: handle range_tree_set failures in alloc/free paths 2026-09-02 9:37 ` [PATCH bpf-next v5 3/3] bpf, arena: handle range_tree_set failures in alloc/free paths chenyuan_fl @ 2026-09-02 9:53 ` sashiko-bot 0 siblings, 0 replies; 15+ messages in thread From: sashiko-bot @ 2026-09-02 9:53 UTC (permalink / raw) To: chenyuan_fl; +Cc: bpf Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: New issues: - [High] If `arena_map_free()` fails to acquire the lock and aborts, it leaks the arena and leaves the worker in an infinite retry loop on `-ENOMEM`. - [Low] The `if (arena->dying)` check in `arena_free_worker` is mathematically dead code. Pre-existing issues: - [High] Virtual memory range is marked free before TLB flush and user PTE zap, allowing use-after-free and data loss. -- commit 9b91373ca20b3a032ab090590fde24790048b705 Author: Yuan Chen <chenyuan@kylinos.cn> bpf, arena: handle range_tree_set failures in alloc/free paths This commit adds error handling for range_tree_set() in the arena allocation and free paths. It includes a bounded retry loop for arena_map_free() to acquire the spinlock and introduces arena->dying to stop the worker thread's retry loop during map teardown. > diff --git a/kernel/bpf/arena.c b/kernel/bpf/arena.c > index 7b6847200b431..b0d1f0facfb2c 100644 > --- a/kernel/bpf/arena.c > +++ b/kernel/bpf/arena.c [ ... ] > @@ -370,6 +373,9 @@ static int existing_page_cb(pte_t *ptep, unsigned long addr, void *data) > static void arena_map_free(struct bpf_map *map) > { > struct bpf_arena *arena = container_of(map, struct bpf_arena, map); > + struct llist_node *list, *pos, *t; > + unsigned long flags; > + int ret, i; > > /* > * Check that user vma-s are not around when bpf map is freed. > @@ -380,7 +386,40 @@ static void arena_map_free(struct bpf_map *map) > if (WARN_ON_ONCE(!list_empty(&arena->vma_list))) > return; > > - /* Ensure no pending deferred frees */ > + /* > + * No fallback if this fails, so retry a few times for a long but > + * finite hold; -EDEADLK can't be waited out. Cap the retries: > + * leaking the arena is better than hanging map free. > + */ > + for (i = 0; i < 10; i++) { > + ret = raw_res_spin_lock_irqsave(&arena->spinlock, flags); > + if (!ret || ret == -EDEADLK) > + break; > + msleep(1); > + } > + if (ret) { > + WARN_ONCE(1, "bpf_arena: spinlock acquire failed %d\n", ret); > + return; > + } [Severity: High] If this early return path triggers, it intentionally leaks the arena, but doesn't set arena->dying. Could this strand the worker in an infinite retry loop? If the worker's range_tree_set() call fails continuously (e.g., due to memory exhaustion), it will endlessly requeue the span and queue an irq_work to retry. Because arena->dying remains false, this could turn into an infinite busy loop. > + /* > + * Set @dying before draining: the worker checks it under this > + * spinlock before requeueing, so a failed span is either stolen > + * here or dropped by the worker. > + */ > + arena->dying = true; [ ... ] > @@ -977,12 +1031,13 @@ 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; > long kaddr, page_cnt, pgoff; > unsigned long flags; > + bool retry = false; > > if (raw_res_spin_lock_irqsave(&arena->spinlock, flags)) { > schedule_work(work); > @@ -992,28 +1047,52 @@ 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); > > + /* > + * Set the range free before clearing PTEs, and requeue the > + * span on failure: the PTEs stay intact and the free is > + * retried later. Only spans moved to @cleared (PTE clearing > + * actually ran) reach the flush/zap/release loop below. > + */ > + if (range_tree_set(&arena->rt, pgoff, page_cnt)) { > + if (arena->dying) { > + /* > + * The map is being freed. PTEs stay intact > + * and the pages are reclaimed by > + * arena_map_free() via existing_page_cb(). > + */ > + kfree_nolock(s); > + continue; > + } [Severity: Low] Is this check mathematically dead code? arena->dying is set to true by arena_map_free() while holding arena->spinlock, after which arena->free_spans is immediately drained via llist_del_all(). In arena_free_worker(), the list is also drained at the very beginning of its arena->spinlock critical section. Because both functions drain the list under the same spinlock, if arena_map_free() runs first, the worker's list is empty and the loop never executes. If the worker runs first, dying is guaranteed to be false throughout its critical section. > + llist_add(&s->node, &arena->free_spans); > + retry = true; > + 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); > > + if (retry) > + irq_work_queue(&arena->free_irq); > + > /* 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; > + kaddr = arena_vm_start + s->uaddr; > + > + /* ensure no stale TLB entries */ > + flush_tlb_kernel_range(kaddr, kaddr + (page_cnt * PAGE_SIZE)); [Severity: High] This isn't a bug introduced by this patch, but dropping the spinlock before the TLB flush opens a race window. Can a concurrent arena_alloc_pages() allocate the newly freed virtual range and map new physical pages to it? Since the TLB flush for the old pages has not yet executed, accesses by the new owner could hit stale TLB entries (or user PTEs). Writes would go to the old physical pages which are later freed, resulting in a use-after-free and data loss. The synchronous free path in arena_free_pages() appears to have the same preexisting vulnerability. > > - /* remove pages from user vmas */ > - zap_pages(arena, full_uaddr, page_cnt); -- Sashiko AI review · https://sashiko.dev/#/patchset/20260902093740.2338724-1-chenyuan_fl@163.com?part=3 ^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH bpf-next v4 3/3] bpf, arena: check range_tree_set return in arena_free_pages and arena_free_worker 2026-08-24 13:40 [PATCH bpf-next v4 0/3] bpf, arena: fix range_tree consistency on allocation failure chenyuan_fl 2026-08-24 13:40 ` [PATCH bpf-next v4 1/3] bpf, arena: fix range_tree_clear inconsistency on kmalloc_nolock failure chenyuan_fl 2026-08-24 13:40 ` [PATCH bpf-next v4 2/3] bpf, arena: fix range_tree_set " chenyuan_fl @ 2026-08-24 13:40 ` chenyuan_fl 2026-08-24 13:54 ` sashiko-bot 2026-08-24 14:35 ` bot+bpf-ci 2 siblings, 2 replies; 15+ messages in thread From: chenyuan_fl @ 2026-08-24 13:40 UTC (permalink / raw) To: bpf Cc: linux-kernel, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis, Ihor Solodrai, Yuan Chen From: Yuan Chen <chenyuan@kylinos.cn> arena_free_pages() and arena_free_worker() now handle range_tree_set() errors. arena_free_pages() aborts the free on error, and arena_free_worker() moves range_tree_set() before PTE clearing so that a failed tree update leaves the PTEs intact instead of freeing pages that the arena free tree does not track. Also check the range_tree_set() return value in arena_alloc_pages()'s error path, which restores the unpopulated tail of a partially allocated range; log a warning instead of silently leaking the virtual range when the tree update fails. range_tree_set() is failure-atomic (it pre-allocates the node before touching the tree), so on -ENOMEM the range stays tracked as allocated and the pages remain mapped and accessible. A failed free is therefore retryable, and arena_map_free() reclaims any retained pages at map destruction; aborting the free avoids clearing PTEs for pages the arena free tree does not track. In arena_free_worker() a failed tree update used to leave the span in the drained list, where the second loop would still flush TLB entries, zap user VMAs, and free the span itself: the free request was dropped, user mappings were destroyed for a free that never happened, and the pages stayed mapped until map destruction. Keep failed spans on arena->free_spans instead and retry them on a later worker run; only spans whose PTE clearing actually ran are flushed, zapped, and released. Suggested-by: Emil Tsalapatis <emil@etsalapatis.com> Signed-off-by: Yuan Chen <chenyuan@kylinos.cn> --- kernel/bpf/arena.c | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/kernel/bpf/arena.c b/kernel/bpf/arena.c index 555ee2531ef9..1315872941e1 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: failed to restore free range %ld+%ld after partial 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, so -ENOMEM leaves the + * range allocated and the pages mapped. Abort the free rather + * than returning pages the free tree does not track; a later + * free of the same range can succeed. + */ + 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,12 +990,13 @@ 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; long kaddr, page_cnt, pgoff; unsigned long flags; + bool retry = false; if (raw_res_spin_lock_irqsave(&arena->spinlock, flags)) { schedule_work(work); @@ -992,28 +1006,43 @@ 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); + /* + * Set the range free before clearing PTEs, and requeue the + * span on failure: the PTEs stay intact and the free is + * retried later. Only spans moved to @cleared (PTE clearing + * actually ran) reach the flush/zap/release loop below. + */ + if (range_tree_set(&arena->rt, pgoff, page_cnt)) { + llist_add(&s->node, &arena->free_spans); + retry = true; + 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); + if (retry) + irq_work_queue(&arena->free_irq); + /* 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 ^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH bpf-next v4 3/3] bpf, arena: check range_tree_set return in arena_free_pages and arena_free_worker 2026-08-24 13:40 ` [PATCH bpf-next v4 3/3] bpf, arena: check range_tree_set return in arena_free_pages and arena_free_worker chenyuan_fl @ 2026-08-24 13:54 ` sashiko-bot 2026-08-24 14:35 ` bot+bpf-ci 1 sibling, 0 replies; 15+ messages in thread From: sashiko-bot @ 2026-08-24 13:54 UTC (permalink / raw) To: chenyuan_fl; +Cc: bpf Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider: New issues: - [Critical] Re-arming irq_work in arena_free_worker() bypasses teardown synchronization, leading to UAF of the bpf_arena structure on map destruction. - [High] Immediate, unconditional retry in arena_free_worker() when range_tree_set() fails creates a tight CPU busy-loop and livelock. - [High] Aborting arena_free_pages() on range_tree_set() failure silently leaks physical memory and memcg quota during the arena's lifetime. Pre-existing issues: - [High] Calling apply_to_existing_page_range() while holding raw_res_spin_lock_irqsave violates lock wait-type nesting rules (LD_WAIT_SPIN > LD_WAIT_CONFIG). -- --- Patch [3]: [PATCH bpf-next v4 3/3] bpf, arena: check range_tree_set return in arena_free_pages and arena_free_worker --- commit 5874fa92054ab81b6a7b82ddf6fae39167bcb8d3 Author: Yuan Chen <chenyuan@kylinos.cn> Subject: bpf, arena: check range_tree_set return in arena_free_pages and arena_free_worker This commit handles errors from range_tree_set() in arena_free_pages() and arena_free_worker(). It aborts the free operation on failure to avoid clearing PTEs for untracked pages, and modifies arena_free_worker() to requeue failed spans for later retry. > diff --git a/kernel/bpf/arena.c b/kernel/bpf/arena.c > index 7b6847200b431..169b2e71e9cbb 100644 > --- a/kernel/bpf/arena.c > +++ b/kernel/bpf/arena.c [ ... ] > @@ -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, so -ENOMEM leaves the > + * range allocated and the pages mapped. Abort the free rather > + * than returning pages the free tree does not track; a later > + * free of the same range can succeed. > + */ > + raw_res_spin_unlock_irqrestore(&arena->spinlock, flags); > + bpf_map_memcg_exit(old_memcg, new_memcg); > + return; > + } [Severity: High] When range_tree_set() fails here, does returning early permanently leak physical pages and memcg quota if this is called from the unwind path in arena_alloc_pages()? If arena_alloc_pages() fails partway through, it calls arena_free_pages() to unwind. If this early return is hit, the physical pages remain mapped and allocated. Since the original allocation returns 0, the BPF program cannot free these pages later. > > init_llist_head(&free_pages); > cdata.arena = arena; [ ... ] > @@ -992,28 +1006,43 @@ 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); > > + /* > + * Set the range free before clearing PTEs, and requeue the > + * span on failure: the PTEs stay intact and the free is > + * retried later. Only spans moved to @cleared (PTE clearing > + * actually ran) reach the flush/zap/release loop below. > + */ > + if (range_tree_set(&arena->rt, pgoff, page_cnt)) { > + llist_add(&s->node, &arena->free_spans); > + retry = true; > + 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); [Severity: High] This isn't a bug introduced by this patch, but does calling apply_to_existing_page_range() here violate lock wait-type nesting rules? This code executes while holding the raw_res_spin_lock_irqsave() raw spinlock. apply_to_existing_page_range() walks page tables and acquires the page table lock (PTL). The PTL is a spinlock_t, which maps to a sleepable rt_mutex on PREEMPT_RT kernels. Acquiring a sleepable lock inside a raw lock can cause a scheduling while atomic bug. > - > - range_tree_set(&arena->rt, pgoff, page_cnt); > + llist_add(&s->node, &cleared); > } > raw_res_spin_unlock_irqrestore(&arena->spinlock, flags); > > + if (retry) > + irq_work_queue(&arena->free_irq); [Severity: High] Can this unconditional retry create a tight CPU busy-loop and livelock? If range_tree_set() fails (e.g., from -ENOMEM due to memory cgroup limits), it sets retry = true and queues arena->free_irq without any backoff. This immediately reschedules the worker. Since no memory was freed, it seems it will likely hit -ENOMEM again indefinitely. [Severity: Critical] Does re-arming irq_work here bypass teardown synchronization and lead to a use-after-free of the bpf_arena structure? During map destruction, arena_map_free() calls flush_work(&arena->free_work) to wait for the worker to finish. However, flush_work() does not wait for newly queued irq_work. If arena_map_free() proceeds to free the arena, the pending IPI might execute arena_free_irq() and schedule_work(), dereferencing the freed arena pointer. > + > /* 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; -- Sashiko AI review · https://sashiko.dev/#/patchset/20260824134016.2006188-1-chenyuan_fl@163.com?part=3 ^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH bpf-next v4 3/3] bpf, arena: check range_tree_set return in arena_free_pages and arena_free_worker 2026-08-24 13:40 ` [PATCH bpf-next v4 3/3] bpf, arena: check range_tree_set return in arena_free_pages and arena_free_worker chenyuan_fl 2026-08-24 13:54 ` sashiko-bot @ 2026-08-24 14:35 ` bot+bpf-ci 1 sibling, 0 replies; 15+ messages in thread From: bot+bpf-ci @ 2026-08-24 14:35 UTC (permalink / raw) To: chenyuan_fl, bpf Cc: linux-kernel, ast, daniel, andrii, eddyz87, memxor, martin.lau, song, yonghong.song, jolsa, emil, ihor.solodrai, chenyuan, ast, andrii, daniel, martin.lau, eddyz87, yonghong.song, clm, ihor.solodrai [-- Attachment #1: Type: text/plain, Size: 2114 bytes --] > bpf, arena: check range_tree_set return in arena_free_pages and arena_free_worker > > arena_free_pages() and arena_free_worker() now handle range_tree_set() > errors. arena_free_pages() aborts the free on error, and > arena_free_worker() moves range_tree_set() before PTE clearing so that a > failed tree update leaves the PTEs intact instead of freeing pages that > the arena free tree does not track. > > Also check the range_tree_set() return value in arena_alloc_pages()'s > error path, which restores the unpopulated tail of a partially allocated > range; log a warning instead of silently leaking the virtual range when > the tree update fails. > > range_tree_set() is failure-atomic (it pre-allocates the node before > touching the tree), so on -ENOMEM the range stays tracked as allocated > and the pages remain mapped and accessible. A failed free is therefore > retryable, and arena_map_free() reclaims any retained pages at map > destruction; aborting the free avoids clearing PTEs for pages the arena > free tree does not track. > > In arena_free_worker() a failed tree update used to leave the span in > the drained list, where the second loop would still flush TLB entries, > zap user VMAs, and free the span itself: the free request was dropped, > user mappings were destroyed for a free that never happened, and the > pages stayed mapped until map destruction. Keep failed spans on > arena->free_spans instead and retry them on a later worker run; only > spans whose PTE clearing actually ran are flushed, zapped, and released. > > Suggested-by: Emil Tsalapatis <emil@etsalapatis.com> > Signed-off-by: Yuan Chen <chenyuan@kylinos.cn> This looks like a bug fix for error handling in the arena code introduced in an earlier commit. Should this include a Fixes: tag pointing to: Fixes: dc11a4dba246 ("bpf: Recover arena kernel faults with scratch page") --- 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/32736729475 ^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2026-09-02 9:53 UTC | newest] Thread overview: 15+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-08-24 13:40 [PATCH bpf-next v4 0/3] bpf, arena: fix range_tree consistency on allocation failure chenyuan_fl 2026-08-24 13:40 ` [PATCH bpf-next v4 1/3] bpf, arena: fix range_tree_clear inconsistency on kmalloc_nolock failure chenyuan_fl 2026-08-24 14:35 ` bot+bpf-ci 2026-08-24 13:40 ` [PATCH bpf-next v4 2/3] bpf, arena: fix range_tree_set " chenyuan_fl 2026-08-24 14:35 ` bot+bpf-ci 2026-08-27 2:56 ` Alexei Starovoitov 2026-09-01 7:01 ` chenyuan 2026-09-02 9:37 ` [PATCH bpf-next v5 0/3] bpf, arena: fix range_tree consistency on allocation failure chenyuan_fl 2026-09-02 9:37 ` [PATCH bpf-next v5 1/3] bpf, arena: fix range_tree_clear inconsistency on kmalloc_nolock failure chenyuan_fl 2026-09-02 9:37 ` [PATCH bpf-next v5 2/3] bpf, arena: fix range_tree_set " chenyuan_fl 2026-09-02 9:37 ` [PATCH bpf-next v5 3/3] bpf, arena: handle range_tree_set failures in alloc/free paths chenyuan_fl 2026-09-02 9:53 ` sashiko-bot 2026-08-24 13:40 ` [PATCH bpf-next v4 3/3] bpf, arena: check range_tree_set return in arena_free_pages and arena_free_worker chenyuan_fl 2026-08-24 13:54 ` sashiko-bot 2026-08-24 14:35 ` bot+bpf-ci
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox