[PATCH bpf v3 2/2] bpf, libbpf: reject non-exclusive metadata maps in the signed loader

Daniel Borkmann daniel at iogearbox.net
Fri May 29 12:25:45 UTC 2026


On 5/23/26 5:12 PM, Alexei Starovoitov wrote:
> On Fri, May 22, 2026 at 11:53 PM KP Singh <kpsingh at kernel.org> wrote:
>>
>> The loader verifies map->sha against the metadata hash in its
>> instructions. map->sha is calculated when BPF_OBJ_GET_INFO_BY_FD is called
>> on the frozen map.
>>
>> While the map is frozen, the loader must also ensure the map is
>> exclusive, as, without exclusivity, another BPF program with map access
>> can mutate the contents afterwards, so the check passes on stale data.
> 
> Hold on. How is this an issue? excl_prog_sha guarantees
> that only loader prog can use this map.
> Are you saying the same loader prog will use the same map
> for the 2nd time. Ok. I still don't see a problem.
> 
>> Place excl_prog_sha right after sha[] in struct bpf_map and have
>> gen_loader bail with -EINVAL when it is NULL, via BPF_PSEUDO_MAP_IDX at
>> fixed offset 32. The 8-byte read of the pointer field limits this to
>> 64-bit kernels; gen_loader needs target pointer size tracking to emit
>> the right sized read on 32-bit (follow-up).
> 
> I don't think we can go from maybe-racy to certainly-broken-on-32-bit.
> So only applied patch 1.

I've looked a bit more into it with regards to above question from Alexei
as well as the __bpf_md_ptr issue.

Imho, KP is correct that the extra check/enforcement is needed. So Alice
as a trusted signer generates the loader program (loader_insns + data_blob)
and signs it. The loader program contains the below enforcement to reject
if the metadata map was not exclusive.

Now the (untrusted) host that wants to load the program, it holds a signed
loader where they can't change a byte of it without breaking the signature.

However, it could simply omit excl_prog_hash on BPF_MAP_CREATE for the data
map (which would "normally" be bound exclusively to the loader).

Then check_map_prog_compatibility() enforcement is skipped on verifier side
given excl_prog_sha is not set. The loader loads fine, the fingerprint check
can then pass against a stale snapshot while a different program mangled the
data_blob underneath.

Regarding __bpf_md_ptr, I would solve it differently via fixed size, see below
together with the excl check coming before the signature check in the loader
and the build bug assertions, and a jmp not eq to 1.

  include/linux/bpf.h        |  1 +
  kernel/bpf/syscall.c       |  5 +++++
  tools/lib/bpf/gen_loader.c | 17 +++++++++++++++++
  3 files changed, 23 insertions(+)

diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index cd191c5fdb0a..487f4653d8a6 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -295,6 +295,7 @@ struct bpf_map_owner {
  
  struct bpf_map {
  	u8 sha[SHA256_DIGEST_SIZE];
+	u32 excl;
  	const struct bpf_map_ops *ops;
  	struct bpf_map *inner_map_meta;
  #ifdef CONFIG_SECURITY
diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
index 630d530782fe..37dacdbc5c01 100644
--- a/kernel/bpf/syscall.c
+++ b/kernel/bpf/syscall.c
@@ -1572,6 +1572,11 @@ static int map_create(union bpf_attr *attr, bpfptr_t uattr)
  			err = -EFAULT;
  			goto free_map;
  		}
+
+		/* See libbpf: emit_signature_match() */
+		BUILD_BUG_ON(offsetof(struct bpf_map, excl) != SHA256_DIGEST_SIZE);
+		BUILD_BUG_ON(offsetof(struct bpf_map, sha)  != 0);
+		map->excl = 1;
  	} else if (attr->excl_prog_hash_size) {
  		err = -EINVAL;
  		goto free_map;
diff --git a/tools/lib/bpf/gen_loader.c b/tools/lib/bpf/gen_loader.c
index bcea21c3b7bb..cd8d7df94ac7 100644
--- a/tools/lib/bpf/gen_loader.c
+++ b/tools/lib/bpf/gen_loader.c
@@ -586,6 +586,23 @@ static void emit_signature_match(struct bpf_gen *gen)
  	__s64 off;
  	int i;
  
+	/*
+	 * Reject if the metadata map is not exclusive. Without exclusivity
+	 * the cached map->sha[] verified above can be stale: another BPF
+	 * program with map access could have mutated the contents between
+	 * BPF_OBJ_GET_INFO_BY_FD and loader execution.
+	 */
+	emit2(gen, BPF_LD_IMM64_RAW_FULL(BPF_REG_1, BPF_PSEUDO_MAP_IDX,
+					 0, 0, 0, 0));
+	emit(gen, BPF_LDX_MEM(BPF_W, BPF_REG_2, BPF_REG_1, SHA256_DIGEST_LENGTH));
+	off = -(gen->insn_cur - gen->insn_start - gen->cleanup_label) / 8 - 2;
+	if (is_simm16(off)) {
+		emit(gen, BPF_MOV64_IMM(BPF_REG_7, -EINVAL));
+		emit(gen, BPF_JMP_IMM(BPF_JNE, BPF_REG_2, 1, off));
+	} else {
+		gen->error = -ERANGE;
+	}
+
  	for (i = 0; i < SHA256_DWORD_SIZE; i++) {
  		emit2(gen, BPF_LD_IMM64_RAW_FULL(BPF_REG_1, BPF_PSEUDO_MAP_IDX,
  						 0, 0, 0, 0));
-- 
2.43.0



More information about the Linux-security-module-archive mailing list