* [PATCH v4 2/2] selftests/bpf: Cover tail-call cgroup storage prog-array checks
2026-06-04 7:03 [PATCH v4 1/2] bpf: Tighten cgroup storage cookie checks for prog arrays Lin Ma
@ 2026-06-04 7:03 ` Lin Ma
2026-06-04 18:08 ` Yonghong Song
2026-06-04 7:48 ` [PATCH v4 1/2] bpf: Tighten cgroup storage cookie checks for prog arrays bot+bpf-ci
2026-06-04 16:56 ` Yonghong Song
2 siblings, 1 reply; 7+ messages in thread
From: Lin Ma @ 2026-06-04 7:03 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, bpf
Cc: Andrii Nakryiko, John Fastabend, Martin KaFai Lau,
Eduard Zingerman, Kumar Kartikeya Dwivedi, Song Liu,
Yonghong Song, Jiri Olsa, YiFei Zhu, Shuah Khan, linux-kselftest,
linux-kernel, Amery Hung, Lin Ma, Rongzhen Cui, Jingguo Tan,
cenxianlong, chenzhe
Add tail-call selftests for prog-array ownership when cgroup storage is
in use. Verify that loading succeeds when callers and callees reuse the
owner's cgroup storage map, and that loading fails for a different
storage map and for the A(storage) -> B(no storage) -> C(storage)
bridge case from patch 1.
Suggested-by: Leon Hwang <leon.hwang@linux.dev>
Signed-off-by: Lin Ma <malin89@huawei.com>
Signed-off-by: Rongzhen Cui <cuirongzhen@huawei.com>
Signed-off-by: Jingguo Tan <tanjingguo@huawei.com>
---
v2: https://lore.kernel.org/bpf/31927f33-9db0-4a39-b38a-72b33487979e@linux.dev/T/#t
v2 -> v3:
- add explicit A(storage) -> B(no storage) -> C(storage) bridge
coverage
v3: https://lore.kernel.org/bpf/7b3e1adae5c92153e991ac406b2d334609c36d866b5bf81e4465cf63bde0a79c@mail.kernel.org/T/#t
v3 -> v4:
- move the new tail-call cgroup storage subtests to the tail of
test_tailcalls()
- add Leon Hwang's Suggested-by tag
---
.../selftests/bpf/prog_tests/tailcalls.c | 159 ++++++++++++++++++
.../bpf/progs/tailcall_cgrp_storage.c | 44 +++++
.../progs/tailcall_cgrp_storage_no_storage.c | 20 +++
.../bpf/progs/tailcall_cgrp_storage_owner.c | 32 ++++
4 files changed, 255 insertions(+)
create mode 100644 tools/testing/selftests/bpf/progs/tailcall_cgrp_storage.c
create mode 100644 tools/testing/selftests/bpf/progs/tailcall_cgrp_storage_no_storage.c
create mode 100644 tools/testing/selftests/bpf/progs/tailcall_cgrp_storage_owner.c
diff --git a/tools/testing/selftests/bpf/prog_tests/tailcalls.c b/tools/testing/selftests/bpf/prog_tests/tailcalls.c
index 7d534fde0af9..e2b2e0e0b1ed 100644
--- a/tools/testing/selftests/bpf/prog_tests/tailcalls.c
+++ b/tools/testing/selftests/bpf/prog_tests/tailcalls.c
@@ -8,6 +8,9 @@
#include "tailcall_freplace.skel.h"
#include "tc_bpf2bpf.skel.h"
#include "tailcall_fail.skel.h"
+#include "tailcall_cgrp_storage_owner.skel.h"
+#include "tailcall_cgrp_storage_no_storage.skel.h"
+#include "tailcall_cgrp_storage.skel.h"
#include "tailcall_sleepable.skel.h"
/* test_tailcall_1 checks basic functionality by patching multiple locations
@@ -1654,6 +1657,154 @@ static void test_tailcall_failure()
RUN_TESTS(tailcall_fail);
}
+static void test_tailcall_cgrp_storage(void)
+{
+ struct tailcall_cgrp_storage_owner *owner_skel = NULL;
+ struct tailcall_cgrp_storage *skel = NULL;
+ int err, key = 0, prog_array_fd, prog_fd, storage_map_fd;
+
+ owner_skel = tailcall_cgrp_storage_owner__open_and_load();
+ if (!ASSERT_OK_PTR(owner_skel, "owner_open_and_load"))
+ return;
+
+ prog_array_fd = bpf_map__fd(owner_skel->maps.prog_array);
+ storage_map_fd = bpf_map__fd(owner_skel->maps.storage_map);
+
+ skel = tailcall_cgrp_storage__open();
+ if (!ASSERT_OK_PTR(skel, "tailcall_cgrp_storage__open"))
+ goto out;
+
+ err = bpf_map__reuse_fd(skel->maps.prog_array, prog_array_fd);
+ if (!ASSERT_OK(err, "reuse_prog_array"))
+ goto out;
+
+ err = bpf_map__reuse_fd(skel->maps.storage_map, storage_map_fd);
+ if (!ASSERT_OK(err, "reuse_storage_map"))
+ goto out;
+
+ err = bpf_object__load(skel->obj);
+ if (!ASSERT_OK(err, "tailcall_cgrp_storage__load"))
+ goto out;
+
+ prog_fd = bpf_program__fd(skel->progs.callee_prog);
+ err = bpf_map_update_elem(prog_array_fd, &key, &prog_fd, BPF_ANY);
+ ASSERT_OK(err, "update_prog_array");
+
+out:
+ tailcall_cgrp_storage__destroy(skel);
+ tailcall_cgrp_storage_owner__destroy(owner_skel);
+}
+
+static void test_tailcall_cgrp_storage_diff_storage(void)
+{
+ struct tailcall_cgrp_storage_owner *owner_skel = NULL;
+ struct tailcall_cgrp_storage *skel = NULL;
+ int err, prog_array_fd;
+
+ owner_skel = tailcall_cgrp_storage_owner__open_and_load();
+ if (!ASSERT_OK_PTR(owner_skel, "owner_open_and_load"))
+ return;
+
+ prog_array_fd = bpf_map__fd(owner_skel->maps.prog_array);
+
+ skel = tailcall_cgrp_storage__open();
+ if (!ASSERT_OK_PTR(skel, "tailcall_cgrp_storage__open"))
+ goto out;
+
+ err = bpf_map__reuse_fd(skel->maps.prog_array, prog_array_fd);
+ if (!ASSERT_OK(err, "reuse_prog_array"))
+ goto out;
+
+ err = bpf_object__load(skel->obj);
+ ASSERT_ERR(err, "tailcall_cgrp_storage__load");
+
+out:
+ tailcall_cgrp_storage__destroy(skel);
+ tailcall_cgrp_storage_owner__destroy(owner_skel);
+}
+
+static void test_tailcall_cgrp_storage_no_storage(void)
+{
+ struct tailcall_cgrp_storage_owner *owner_skel = NULL;
+ struct tailcall_cgrp_storage_no_storage *skel = NULL;
+ int err, prog_array_fd;
+
+ owner_skel = tailcall_cgrp_storage_owner__open_and_load();
+ if (!ASSERT_OK_PTR(owner_skel, "owner_open_and_load"))
+ return;
+
+ prog_array_fd = bpf_map__fd(owner_skel->maps.prog_array);
+
+ skel = tailcall_cgrp_storage_no_storage__open();
+ if (!ASSERT_OK_PTR(skel, "tailcall_cgrp_storage_no_storage__open"))
+ goto out;
+
+ err = bpf_map__reuse_fd(skel->maps.prog_array, prog_array_fd);
+ if (!ASSERT_OK(err, "reuse_prog_array"))
+ goto out;
+
+ err = bpf_object__load(skel->obj);
+ ASSERT_ERR(err, "tailcall_cgrp_storage_no_storage__load");
+
+out:
+ tailcall_cgrp_storage_no_storage__destroy(skel);
+ tailcall_cgrp_storage_owner__destroy(owner_skel);
+}
+
+static void test_tailcall_cgrp_storage_no_storage_bridge(void)
+{
+ struct tailcall_cgrp_storage_owner *owner_skel = NULL;
+ struct tailcall_cgrp_storage_no_storage *bridge_skel = NULL;
+ struct tailcall_cgrp_storage *callee_skel = NULL;
+ int err, key = 0, prog_array_fd, prog_fd, storage_map_fd;
+
+ owner_skel = tailcall_cgrp_storage_owner__open_and_load();
+ if (!ASSERT_OK_PTR(owner_skel, "owner_open_and_load"))
+ return;
+
+ prog_array_fd = bpf_map__fd(owner_skel->maps.prog_array);
+ storage_map_fd = bpf_map__fd(owner_skel->maps.storage_map);
+
+ callee_skel = tailcall_cgrp_storage__open();
+ if (!ASSERT_OK_PTR(callee_skel, "tailcall_cgrp_storage__open"))
+ goto out;
+
+ bpf_program__set_autoload(callee_skel->progs.caller_prog, false);
+
+ err = bpf_map__reuse_fd(callee_skel->maps.prog_array, prog_array_fd);
+ if (!ASSERT_OK(err, "reuse_prog_array"))
+ goto out;
+
+ err = bpf_map__reuse_fd(callee_skel->maps.storage_map, storage_map_fd);
+ if (!ASSERT_OK(err, "reuse_storage_map"))
+ goto out;
+
+ err = bpf_object__load(callee_skel->obj);
+ if (!ASSERT_OK(err, "tailcall_cgrp_storage__load"))
+ goto out;
+
+ prog_fd = bpf_program__fd(callee_skel->progs.callee_prog);
+ err = bpf_map_update_elem(prog_array_fd, &key, &prog_fd, BPF_ANY);
+ if (!ASSERT_OK(err, "update_prog_array"))
+ goto out;
+
+ bridge_skel = tailcall_cgrp_storage_no_storage__open();
+ if (!ASSERT_OK_PTR(bridge_skel, "tailcall_cgrp_storage_no_storage__open"))
+ goto out;
+
+ err = bpf_map__reuse_fd(bridge_skel->maps.prog_array, prog_array_fd);
+ if (!ASSERT_OK(err, "reuse_prog_array"))
+ goto out;
+
+ err = bpf_object__load(bridge_skel->obj);
+ ASSERT_ERR(err, "tailcall_cgrp_storage_no_storage_bridge__load");
+
+out:
+ tailcall_cgrp_storage_no_storage__destroy(bridge_skel);
+ tailcall_cgrp_storage__destroy(callee_skel);
+ tailcall_cgrp_storage_owner__destroy(owner_skel);
+}
+
noinline void uprobe_sleepable_trigger(void)
{
asm volatile ("");
@@ -1781,4 +1932,12 @@ void test_tailcalls(void)
test_tailcall_failure();
if (test__start_subtest("tailcall_sleepable"))
test_tailcall_sleepable();
+ if (test__start_subtest("tailcall_cgrp_storage"))
+ test_tailcall_cgrp_storage();
+ if (test__start_subtest("tailcall_cgrp_storage_diff_storage"))
+ test_tailcall_cgrp_storage_diff_storage();
+ if (test__start_subtest("tailcall_cgrp_storage_no_storage"))
+ test_tailcall_cgrp_storage_no_storage();
+ if (test__start_subtest("tailcall_cgrp_storage_no_storage_bridge"))
+ test_tailcall_cgrp_storage_no_storage_bridge();
}
diff --git a/tools/testing/selftests/bpf/progs/tailcall_cgrp_storage.c b/tools/testing/selftests/bpf/progs/tailcall_cgrp_storage.c
new file mode 100644
index 000000000000..4dd3a0033d75
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/tailcall_cgrp_storage.c
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+
+struct {
+ __uint(type, BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE);
+ __type(key, struct bpf_cgroup_storage_key);
+ __type(value, __u64);
+} storage_map SEC(".maps");
+
+struct {
+ __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
+ __uint(max_entries, 1);
+ __uint(key_size, sizeof(__u32));
+ __uint(value_size, sizeof(__u32));
+} prog_array SEC(".maps");
+
+SEC("cgroup_skb/egress")
+int caller_prog(struct __sk_buff *skb)
+{
+ __u64 *storage;
+
+ storage = bpf_get_local_storage(&storage_map, 0);
+ if (storage)
+ *storage = 1;
+
+ bpf_tail_call(skb, &prog_array, 0);
+ return 1;
+}
+
+SEC("cgroup_skb/egress")
+int callee_prog(struct __sk_buff *skb)
+{
+ __u64 *storage;
+
+ storage = bpf_get_local_storage(&storage_map, 0);
+ if (storage)
+ *storage = 1;
+
+ return 1;
+}
+
+char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/progs/tailcall_cgrp_storage_no_storage.c b/tools/testing/selftests/bpf/progs/tailcall_cgrp_storage_no_storage.c
new file mode 100644
index 000000000000..f95166dad7c5
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/tailcall_cgrp_storage_no_storage.c
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+
+struct {
+ __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
+ __uint(max_entries, 1);
+ __uint(key_size, sizeof(__u32));
+ __uint(value_size, sizeof(__u32));
+} prog_array SEC(".maps");
+
+SEC("cgroup_skb/egress")
+int caller_prog(struct __sk_buff *skb)
+{
+ bpf_tail_call(skb, &prog_array, 0);
+ return 1;
+}
+
+char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/progs/tailcall_cgrp_storage_owner.c b/tools/testing/selftests/bpf/progs/tailcall_cgrp_storage_owner.c
new file mode 100644
index 000000000000..d7e8ec9855c5
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/tailcall_cgrp_storage_owner.c
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+
+struct {
+ __uint(type, BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE);
+ __type(key, struct bpf_cgroup_storage_key);
+ __type(value, __u64);
+} storage_map SEC(".maps");
+
+struct {
+ __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
+ __uint(max_entries, 1);
+ __uint(key_size, sizeof(__u32));
+ __uint(value_size, sizeof(__u32));
+} prog_array SEC(".maps");
+
+SEC("cgroup_skb/egress")
+int prog_array_owner(struct __sk_buff *skb)
+{
+ __u64 *storage;
+
+ storage = bpf_get_local_storage(&storage_map, 0);
+ if (storage)
+ *storage = 1;
+
+ bpf_tail_call(skb, &prog_array, 0);
+ return 1;
+}
+
+char _license[] SEC("license") = "GPL";
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* Re: [PATCH v4 1/2] bpf: Tighten cgroup storage cookie checks for prog arrays
2026-06-04 7:03 [PATCH v4 1/2] bpf: Tighten cgroup storage cookie checks for prog arrays Lin Ma
2026-06-04 7:03 ` [PATCH v4 2/2] selftests/bpf: Cover tail-call cgroup storage prog-array checks Lin Ma
@ 2026-06-04 7:48 ` bot+bpf-ci
2026-06-04 16:56 ` Yonghong Song
2 siblings, 0 replies; 7+ messages in thread
From: bot+bpf-ci @ 2026-06-04 7:48 UTC (permalink / raw)
To: malin89, ast, daniel, bpf
Cc: andrii, john.fastabend, martin.lau, eddyz87, memxor, song,
yonghong.song, jolsa, zhuyifei, shuah, linux-kselftest,
linux-kernel, ameryhung, malin89, cuirongzhen, tanjingguo,
cenxianlong, chenzhe, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2589 bytes --]
> diff --git a/kernel/bpf/core.c b/kernel/bpf/core.c
> index a656a8572bdb..d85869544dc8 100644
> --- a/kernel/bpf/core.c
> +++ b/kernel/bpf/core.c
> @@ -2480,8 +2480,12 @@ static bool __bpf_prog_map_compatible(struct bpf_map *map,
> break;
> cookie = aux->cgroup_storage[i] ?
> aux->cgroup_storage[i]->cookie : 0;
> - ret = map->owner->storage_cookie[i] == cookie ||
> - !cookie;
> + /*
> + * Tail calls keep using the caller cgroup storage
> + * context, so prog-array members must use the same
> + * storage cookie.
> + */
> + ret = map->owner->storage_cookie[i] == cookie;
Does dropping the "!cookie" term here reject a configuration that was
previously valid and safe?
With the old check, a program that uses no cgroup local storage (cookie
== 0) could join a prog-array whose owner uses cgroup storage
(storage_cookie[i] != 0), because "!cookie" was true. With the new check,
"storage_cookie[i] == 0" is false for that owner, so the storage-less
program is now rejected with -EINVAL:
prog_fd_array_get_ptr() (kernel/bpf/arraymap.c)
bpf_prog_map_compatible() (kernel/bpf/core.c)
__bpf_prog_map_compatible()
The original fix in abad3d0bad72 ("bpf: Fix oob access in cgroup local
storage") appears to document this allowance as intended, describing case
(ii) as "allow the tail call combination if the second program was not
using any of the cgroup local storage maps."
The out-of-bounds access that fix targets happens when a program that
calls bpf_get_local_storage() runs under a mismatched-size entry-program
run context. bpf_get_local_storage() in kernel/bpf/cgroup.c reads the
entry program's pinned run context:
storage = this_cpu_read(bpf_cgroup_storage[stype]);
A program that uses no cgroup local storage never calls that helper, so
can it actually trigger the out-of-bounds access?
For the A -> B(no storage) -> C(storage) path in the commit message, would
C still need to match the owner cookie to join the prog-array, since
C's cookie is nonzero and "!cookie" is false for it? If so, is the bridge
path that this patch aims to close already blocked by the existing check
on C?
This change is Cc: stable and applies under CONFIG_CGROUP_BPF=y, so could
it break existing BPF applications that mix storage-using and storage-less
programs in a single prog-array?
---
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/26936884530
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: [PATCH v4 1/2] bpf: Tighten cgroup storage cookie checks for prog arrays
2026-06-04 7:03 [PATCH v4 1/2] bpf: Tighten cgroup storage cookie checks for prog arrays Lin Ma
2026-06-04 7:03 ` [PATCH v4 2/2] selftests/bpf: Cover tail-call cgroup storage prog-array checks Lin Ma
2026-06-04 7:48 ` [PATCH v4 1/2] bpf: Tighten cgroup storage cookie checks for prog arrays bot+bpf-ci
@ 2026-06-04 16:56 ` Yonghong Song
2026-06-05 23:16 ` Daniel Borkmann
2 siblings, 1 reply; 7+ messages in thread
From: Yonghong Song @ 2026-06-04 16:56 UTC (permalink / raw)
To: Lin Ma, Alexei Starovoitov, Daniel Borkmann, bpf
Cc: Andrii Nakryiko, John Fastabend, Martin KaFai Lau,
Eduard Zingerman, Kumar Kartikeya Dwivedi, Song Liu, Jiri Olsa,
YiFei Zhu, Shuah Khan, linux-kselftest, linux-kernel, Amery Hung,
Rongzhen Cui, Jingguo Tan, cenxianlong, chenzhe
On 6/4/26 12:03 AM, Lin Ma wrote:
> The recent KCTF-reported cgroup local storage issue assigned
> CVE-2025-38502 was fixed by commit abad3d0bad72 ("bpf: Fix oob access
> in cgroup local storage").
>
> However, the previous fixes are still incomplete. The current prog-array
> compatibility check treats a program with no cgroup storage as
> compatible with any stored storage cookie. This allows a storage-less
> program to bridge a tail-call chain between an entry program and a
> storage-using callee even though runtime cgroup local storage still
> follows the caller context.
>
> Require exact per-type storage_cookie equality when checking prog-array
> compatibility. This blocks zero-storage bridge programs from joining a
> prog-array owned by a storage-using program and closes the residual
> A -> B(no storage) -> C(storage) path.
>
> This also aligns with Amery Hung's earlier NULL-storage tail-call fix by
> requiring storage use to match consistently across prog-array users.
>
> Cc: stable@vger.kernel.org
> Fixes: abad3d0bad72 ("bpf: Fix oob access in cgroup local storage")
> Tested-by: Amery Hung <ameryhung@gmail.com>
> Signed-off-by: Lin Ma <malin89@huawei.com>
> Signed-off-by: Rongzhen Cui <cuirongzhen@huawei.com>
> Signed-off-by: Jingguo Tan <tanjingguo@huawei.com>
Acked-by: Yonghong Song <yonghong.song@linux.dev>
> ---
> v1: https://lore.kernel.org/bpf/20260601095158.1186318-1-malin89@huawei.com/
> v1 -> v2:
> - refine the commit message and mention the relation to Amery Hung's
> NULL-storage tail-call fix
> - add patch 2/2 selftests for tail-call cgroup storage prog-array
> checks
> v2: https://lore.kernel.org/bpf/31927f33-9db0-4a39-b38a-72b33487979e@linux.dev/T/#t
> v2 -> v3:
> - use abad3d0bad72 as the Fixes tag
> v3: https://lore.kernel.org/bpf/7b3e1adae5c92153e991ac406b2d334609c36d866b5bf81e4465cf63bde0a79c@mail.kernel.org/T/#t
> v3 -> v4:
> - no changes
> ---
> kernel/bpf/core.c | 8 ++++++--
> 1 file changed, 6 insertions(+), 2 deletions(-)
>
> diff --git a/kernel/bpf/core.c b/kernel/bpf/core.c
> index 6aa2a8b24030..f0b61b10f30e 100644
> --- a/kernel/bpf/core.c
> +++ b/kernel/bpf/core.c
> @@ -2470,8 +2470,12 @@ static bool __bpf_prog_map_compatible(struct bpf_map *map,
> break;
> cookie = aux->cgroup_storage[i] ?
> aux->cgroup_storage[i]->cookie : 0;
> - ret = map->owner->storage_cookie[i] == cookie ||
> - !cookie;
> + /*
> + * Tail calls keep using the caller cgroup storage
> + * context, so prog-array members must use the same
> + * storage cookie.
> + */
> + ret = map->owner->storage_cookie[i] == cookie;
> }
> if (ret &&
> map->owner->attach_func_proto != aux->attach_func_proto) {
^ permalink raw reply [flat|nested] 7+ messages in thread