bpf: Free special fields when update [lru_,]percpu_hash maps
Subsystems (3)
v5 cover bpf: Free special fields when update [lru_,]percpu_hash maps
In the discussion thread
"[PATCH bpf-next v9 0/7] bpf: Introduce BPF_F_CPU and BPF_F_ALL_CPUS flags for percpu maps"[1], it was pointed out that missing calls to bpf_obj_free_fields() could
lead to memory leaks.
A selftest was added to confirm that this is indeed a real issue - the refcount of BPF_KPTR_REF field is not decremented when bpf_obj_free_fields() is missing after copy_map_value[,_long]().
Further inspection of copy_map_value[,_long]() call sites revealed two locations affected by this issue:
1. pcpu_copy_value()
2. htab_map_update_elem() when used with BPF_F_LOCK
Similar case happens when update local storage maps with BPF_F_LOCK.
This series fixes the cases where BPF_F_LOCK is not involved by properly calling bpf_obj_free_fields() after copy_map_value[,_long](), and adds a selftest to verify the fix.
The remaining cases involving BPF_F_LOCK will be addressed in a separate patch set after the series
"bpf: Introduce BPF_F_CPU and BPF_F_ALL_CPUS flags for percpu maps" is applied.
Changes:
v4 -> v5:
* Use a local variable to store the this_cpu_ptr()/per_cpu_ptr() result, and reuse it between copy_map_value[,_long]() and bpf_obj_free_fields() in patch #1 (per Andrii).
* Drop patch #2 and #3, because the combination of BPF_F_LOCK with other special fields (except for BPF_SPIN_LOCK) will be disallowed on the UAPI side in the future (per Alexei).
* v4: https://lore.kernel.org/bpf/[email protected]/
v3 -> v4:
* Target bpf-next tree.
* Address comments from Amery:
* Drop 'bpf_obj_free_fields()' in the path of updating local storage maps without BPF_F_LOCK.
* Drop the corresponding self test.
* Respin the other test of local storage maps using syscall BPF programs.
* v3: https://lore.kernel.org/bpf/[email protected]/
v2 -> v3:
* Free special fields when update local storage maps without BPF_F_LOCK.
* Add test to verify decrementing refcount when update cgroup local storage maps without BPF_F_LOCK.
* Address review from AI bot:
* Slow path with BPF_F_LOCK (around line 642-646) in
'bpf_local_storage.c'.
* v2: https://lore.kernel.org/bpf/[email protected]/
v1 -> v2:
* Add test to verify decrementing refcount when update cgroup local storage maps with BPF_F_LOCK.
* Address review from AI bot:
* Fast path without bucket lock (around line 610) in
'bpf_local_storage.c'.
* v1: https://lore.kernel.org/bpf/[email protected]/
Leon Hwang (2):
bpf: Free special fields when update [lru_,]percpu_hash maps selftests/bpf: Add test to verify freeing the special fields when
update [lru_,]percpu_hash maps
kernel/bpf/hashtab.c | 10 +++- .../bpf/prog_tests/refcounted_kptr.c | 57 ++++++++++++++++++ .../selftests/bpf/progs/refcounted_kptr.c | 60 +++++++++++++++++++ 3 files changed, 125 insertions(+), 2 deletions(-)
--
2.51.1
PATCH v5 1/2 bpf: Free special fields when update [lru_,]percpu_hash maps
As [lru_,]percpu_hash maps support BPF_KPTR_{REF,PERCPU}, missing calls to 'bpf_obj_free_fields()' in 'pcpu_copy_value()' could cause the memory referenced by BPF_KPTR_{REF,PERCPU} fields to be held until the map gets freed.
Fix this by calling 'bpf_obj_free_fields()' after 'copy_map_value[,_long]()' in 'pcpu_copy_value()'.
--- kernel/bpf/hashtab.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-)
kernel/bpf/hashtab.c +8 -2
@@ -934,15 +934,21 @@ static void free_htab_elem(struct bpf_htab *htab, struct htab_elem *l)static void pcpu_copy_value (struct bpf_htab *htab,void __percpu *pptr,void *value,bool onallcpus) { +void *ptr; +if (!onallcpus) {/* copy true value_size bytes */ -copy_map_value (&htab->map,this_cpu_ptr (pptr), value); + ptr =this_cpu_ptr (pptr); +copy_map_value (&htab->map, ptr, value); +bpf_obj_free_fields (htab->map.record, ptr); }else {u32 size =round_up (htab->map.value_size,8 );int off =0 , cpu;for_each_possible_cpu (cpu) { -copy_map_value_long (&htab->map,per_cpu_ptr (pptr, cpu), value + off); + ptr =per_cpu_ptr (pptr, cpu); +copy_map_value_long (&htab->map, ptr, value + off); +bpf_obj_free_fields (htab->map.record, ptr); off += size; } }
Signature · 1 line
2.51.1
PATCH v5 2/2 selftests/bpf: Add test to verify freeing the special fields when update [lru_,]percpu_hash maps
In reply to Leon HwangAdd test to verify that updating [lru_,]percpu_hash maps decrements refcount when BPF_KPTR_REF objects are involved.
The tests perform the following steps:
1. Call update_elem() to insert an initial value.
2. Use bpf_refcount_acquire() to increment the refcount.
3. Store the node pointer in the map value.
4. Add the node to a linked list.
5. Probe-read the refcount and verify it is *2*.
6. Call update_elem() again to trigger refcount decrement.
7. Probe-read the refcount and verify it is *1*.
--- .../bpf/prog_tests/refcounted_kptr.c | 57 ++++++++++++++++++ .../selftests/bpf/progs/refcounted_kptr.c | 60 +++++++++++++++++++ 2 files changed, 117 insertions(+)
tools/testing/selftests/bpf/prog_tests/refcounted_kptr.c +57 -0
@@ -44,3 +44,60 @@ void test_refcounted_kptr_wrong_owner(void)ASSERT_OK (opts.retval,"rbtree_wrong_owner_remove_fail_a2 retval" );refcounted_kptr__destroy (skel); } + +void test_percpu_hash_kptr_refcount_leak (void ) +{ +struct refcounted_kptr *skel; +int cpu_nr, fd, err, key =0 ; +struct bpf_map *map; +size_t values_sz; +u64 *values; +LIBBPF_OPTS (bpf_test_run_opts, opts, + .data_in = &pkt_v4, + .data_size_in = sizeof(pkt_v4), + .repeat = 1, + ); + + cpu_nr =libbpf_num_possible_cpus (); +if (!ASSERT_GT (cpu_nr,0 ,"libbpf_num_possible_cpus" )) +return ; + + values =calloc (cpu_nr,sizeof (u64)); +if (!ASSERT_OK_PTR (values,"calloc values" )) +return ; + + skel =refcounted_kptr__open_and_load (); +if (!ASSERT_OK_PTR (skel,"refcounted_kptr__open_and_load" )) { +free (values); +return ; + } + + values_sz = cpu_nr *sizeof (u64); +memset (values,0 , values_sz); + + map = skel->maps.percpu_hash; + err =bpf_map__update_elem (map, &key,sizeof (key), values, values_sz,0 ); +if (!ASSERT_OK (err,"bpf_map__update_elem" )) + goto out; + + fd =bpf_program__fd (skel->progs.percpu_hash_refcount_leak); + err =bpf_prog_test_run_opts (fd, &opts); +if (!ASSERT_OK (err,"bpf_prog_test_run_opts" )) + goto out; +if (!ASSERT_EQ (opts.retval,2 ,"opts.retval" )) + goto out; + + err =bpf_map__update_elem (map, &key,sizeof (key), values, values_sz,0 ); +if (!ASSERT_OK (err,"bpf_map__update_elem" )) + goto out; + + fd =bpf_program__fd (skel->progs.check_percpu_hash_refcount); + err =bpf_prog_test_run_opts (fd, &opts); +ASSERT_OK (err,"bpf_prog_test_run_opts" ); +ASSERT_EQ (opts.retval,1 ,"opts.retval" ); + +out: +refcounted_kptr__destroy (skel); +free (values); +} +
tools/testing/selftests/bpf/progs/refcounted_kptr.c +60 -0
64 lines not on this page · raw message
Signature · 1 line
2.51.1
[email protected]
AI reviewRe: [PATCH bpf-next v5 2/2] selftests/bpf: Add test to verify freeing the special fields when update [lru_,]percpu_hash maps
In __insert_in_list(), after bpf_kptr_xchg() stores the new object in the map and returns the old value in n, can the error path drop both n and m? At this point, the new object (pointed to by m) is…
Quoted 4 lines
tools/testing/selftests/bpf/progs/refcounted_kptr.c +0
-0
[ ... ]
Quoted 20 lines
tools/testing/selftests/bpf/progs/refcounted_kptr.c +16
-0@@ -568,4 +568,64 @@ int BPF_PROG(rbtree_sleepable_rcu_no_explicit_rcu_lock,return 0 ; } +private(kptr_ref) u64 ref; + +static int probe_read_refcount (void ) +{ +u32 refcount; + +bpf_probe_read_kernel (&refcount,sizeof (refcount), (void *) ref); +return refcount; +} + +static int __insert_in_list (struct bpf_list_head *head,struct bpf_spin_lock *lock, + struct node_data __kptr **node) +{ +struct node_data *n, *m; + + n =bpf_obj_new (typeof (*n));
tools/testing/selftests/bpf/progs/refcounted_kptr.c int BPF_PROG(rbtree_sleepable_rcu_no_explicit_rcu_lock, · line 596
Quoted 10 lines
tools/testing/selftests/bpf/progs/refcounted_kptr.c +10
-0+if (!n) +return -1 ; + + m =bpf_refcount_acquire (n); + n =bpf_kptr_xchg (node, n); +if (n) { +bpf_obj_drop (n); +bpf_obj_drop (m); +return -2 ; + }
In __insert_in_list(), after bpf_kptr_xchg() stores the new object in the map and returns the old value in n, can the error path drop both n and m? At this point, the new object (pointed to by m) is already referenced by the map. Dropping m here would free an object that the map still points to, leaving a dangling pointer.
The test itself never triggers this path since it initializes the map with zeros first, so the kptr field starts as NULL. However, if this test were extended or this pattern copied elsewhere, the incorrect error handling could lead to a use-after-free.
tools/testing/selftests/bpf/progs/refcounted_kptr.c int BPF_PROG(rbtree_sleepable_rcu_no_explicit_rcu_lock, · line 603
Quoted 7 lines
tools/testing/selftests/bpf/progs/refcounted_kptr.c +7
-0+ +bpf_spin_lock (lock); +bpf_list_push_front (head, &m->l); + ref = (u64 )(void *) &m->ref; +bpf_spin_unlock (lock); +return probe_read_refcount (); +}
[ ... ]
---
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/19072261328
Re: [PATCH bpf-next v5 2/2] selftests/bpf: Add test to verify freeing the special fields when update [lru_,]percpu_hash maps
Quoted 36 lines · On Tue, Nov 4, 2025 at 6:52 AM <[email protected]> wrote:
Quoted 5 lines
tools/testing/selftests/bpf/progs/refcounted_kptr.c +0
-0[ ... ]
Quoted 28 lines
tools/testing/selftests/bpf/progs/refcounted_kptr.c +24
-0@@ -568,4 +568,64 @@ int BPF_PROG(rbtree_sleepable_rcu_no_explicit_rcu_lock,return 0 ; } +private(kptr_ref) u64 ref; + +static int probe_read_refcount (void ) +{ +u32 refcount; + +bpf_probe_read_kernel (&refcount,sizeof (refcount), (void *) ref); +return refcount; +} + +static int __insert_in_list (struct bpf_list_head *head,struct bpf_spin_lock *lock, + struct node_data __kptr **node) +{ +struct node_data *n, *m; + + n =bpf_obj_new (typeof (*n)); +if (!n) +return -1 ; + + m =bpf_refcount_acquire (n); + n =bpf_kptr_xchg (node, n); +if (n) { +bpf_obj_drop (n); +bpf_obj_drop (m);
Quoted 8 lines
tools/testing/selftests/bpf/progs/refcounted_kptr.c int BPF_PROG(rbtree_sleepable_rcu_no_explicit_rcu_lock, · line 596
Quoted 2 lines
tools/testing/selftests/bpf/progs/refcounted_kptr.c +2
-0+return -2 ; + }In
__insert_in_list(), afterbpf_kptr_xchg()stores the new object in the map and returns the old value in n, can the error path drop both n and m? At this point, the new object (pointed to by m) is already referenced by the map. Dropping m here would free an object that the map still points to, leaving a dangling pointer.
AI is wrong, but I bet it got confused by reuse of variable 'n'. It's hard for humans too.
Leon,
please use a different var.
n = bpf_kptr_xchg(node, n); is a head scratcher.
Also see Yonghong's comment on v4 which I suspect applies to v5.
pw-bot: cr
Re: [PATCH bpf-next v5 2/2] selftests/bpf: Add test to verify freeing the special fields when update [lru_,]percpu_hash maps
Quoted 46 lines · Alexei Starovoitov
Quoted 36 lines · On Tue, Nov 4, 2025 at 6:52 AM <[email protected]> wrote:
Quoted 5 lines
tools/testing/selftests/bpf/progs/refcounted_kptr.c +0
-0[ ... ]
Quoted 28 lines
tools/testing/selftests/bpf/progs/refcounted_kptr.c +24
-0@@ -568,4 +568,64 @@ int BPF_PROG(rbtree_sleepable_rcu_no_explicit_rcu_lock,return 0 ; } +private(kptr_ref) u64 ref; + +static int probe_read_refcount (void ) +{ +u32 refcount; + +bpf_probe_read_kernel (&refcount,sizeof (refcount), (void *) ref); +return refcount; +} + +static int __insert_in_list (struct bpf_list_head *head,struct bpf_spin_lock *lock, + struct node_data __kptr **node) +{ +struct node_data *n, *m; + + n =bpf_obj_new (typeof (*n)); +if (!n) +return -1 ; + + m =bpf_refcount_acquire (n); + n =bpf_kptr_xchg (node, n); +if (n) { +bpf_obj_drop (n); +bpf_obj_drop (m);Quoted 8 lines
tools/testing/selftests/bpf/progs/refcounted_kptr.c int BPF_PROG(rbtree_sleepable_rcu_no_explicit_rcu_lock, · line 596
Quoted 2 lines
tools/testing/selftests/bpf/progs/refcounted_kptr.c +2
-0+return -2 ; + }In
__insert_in_list(), afterbpf_kptr_xchg()stores the new object in the map and returns the old value in n, can the error path drop both n and m? At this point, the new object (pointed to by m) is already referenced by the map. Dropping m here would free an object that the map still points to, leaving a dangling pointer.
Quoted 5 lines
AI is wrong, but I bet it got confused by reuse of variable 'n'. It's hard for humans too.
Leon,
please use a different var.
n =bpf_kptr_xchg(node, n); is a head scratcher.
No problem.
I'll update the variable names in the next revision.
Quoted 2 lines
Also see Yonghong's comment on v4 which I suspect applies to v5.
That was actually a misunderstanding — he didn't run the newly added tests.
Still, I'll update the test name to include "refcounted_kptr" to make it clearer and help avoid such confusion in the future.
Thanks,
Leon