From: Daniel Borkmann <daniel@iogearbox.net>
To: ast@kernel.org
Cc: kpsingh@kernel.org, James.Bottomley@hansenpartnership.com,
paul@paul-moore.com, bboscaccy@linux.microsoft.com,
memxor@gmail.com, torvalds@linux-foundation.org,
bpf@vger.kernel.org, linux-security-module@vger.kernel.org
Subject: [PATCH bpf-next 1/5] bpf: Verify signed loader metadata at load time
Date: Thu, 11 Jun 2026 01:03:25 +0200 [thread overview]
Message-ID: <20260610230329.727075-2-daniel@iogearbox.net> (raw)
In-Reply-To: <20260610230329.727075-1-daniel@iogearbox.net>
A signed gen_loader program carries the programs, maps and relocations it
installs in a metadata array map. The loader instructions are covered by
the PKCS#7 signature, but the metadata map is not: Today the loader
compares the map contents from within BPF against a hash baked into its
(signed) instructions, using the kernel-cached map hash. The kernel itself
never actually attests that the metadata the loader installs is the
metadata that was signed.
This split is the core of the long-standing objection to the BPF signing
scheme from the LSM / integrity side: the integrity check of a light
skeleton only completes once the loader program runs, that is, after the
security_bpf_prog_load() hook, so at admission time an LSM observes a
program whose payload has not yet been verified [0]. Auditing the chain
link is also not a purely cryptographic operation: whoever signs or reviews
an lskel has to disassemble the loader's preamble to convince themselves
that the embedded hash check is present and correct [1][2]. Two acceptable
fixes were identified in those threads: Complete the integrity check
before the admission hook fires, or add a second hook that collects the
verification result after the loader ran [3]. Let's implement the former,
without growing the UAPI.
A signed loader binds its metadata map(s) through the existing fd_array,
and an exclusive map is already bound to a program digest (excl_prog_hash).
So when a signature is present, collect the exclusive maps from fd_array
and append their frozen contents to the instructions before verification:
the signature now covers insns || metadata_0 || metadata_1 || [...] in the
fd_array order, and verification completes in bpf_prog_load() before the
LSM admission hook and before the verifier runs.
A program is either BPF_SIG_UNSIGNED or BPF_SIG_VERIFIED, with nothing in
between. While collecting the fd_array maps, a non-exclusive map bound to
a signed program is rejected, so every map folded into the signature is
exclusive. A signed loader that fails to cover its metadata thus does not
load, and BPF_SIG_VERIFIED always means the instructions and every
exclusive map are authentic.
The maps must be frozen so the hashed bytes cannot change before the
loader runs; the map <-> program digest binding is enforced by the
verifier for every used map. Binding maps through fd_array_cnt makes the
verifier resolve and excl-check them (excl_prog_sha vs prog->digest)
before it would otherwise compute the digest, so compute prog->digest
up front in bpf_prog_load(), over the unmodified instructions the
signature covers, for a load that folds metadata.
Unsigned programs are not affected. Note, signed loaders generated by
older libbpf/bpftool versions need to be regenerated; some of the recent
fixes we've had on the signed loader side require the latter already to
close gaps.
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Link: https://lore.kernel.org/bpf/CAHC9VhSDkwGgPfrBUh7EgBKEJj_JjnY68c0YAmuuLT_i--GskQ@mail.gmail.com [0]
Link: https://lore.kernel.org/bpf/2f71d6c03698eb17d51f7247efde777627ee578a.camel@HansenPartnership.com [1]
Link: https://lore.kernel.org/lkml/ecf0521ed302db672672ebfbc670ecfba36a6e00.camel@HansenPartnership.com [2]
Link: https://lore.kernel.org/bpf/88703f00d5b7a779728451008626efa45e42db3d.camel@HansenPartnership.com [3]
---
kernel/bpf/syscall.c | 164 +++++++++++++++++++++++++++++++++++++++++--
1 file changed, 157 insertions(+), 7 deletions(-)
diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
index d4188a992bd8..796e28e840d6 100644
--- a/kernel/bpf/syscall.c
+++ b/kernel/bpf/syscall.c
@@ -2895,13 +2895,81 @@ static enum bpf_sig_keyring bpf_classify_keyring(s32 keyring_id)
}
}
+static void bpf_prog_put_excl_maps(struct bpf_map **maps, u32 cnt)
+{
+ u32 i;
+
+ if (!maps)
+ return;
+ for (i = 0; i < cnt; i++)
+ bpf_map_put(maps[i]);
+ kfree(maps);
+}
+
+static int bpf_prog_collect_excl_maps(union bpf_attr *attr, bool is_kernel,
+ struct bpf_map ***mapsp, u32 *cntp)
+{
+ bpfptr_t fds = make_bpfptr(attr->fd_array, is_kernel);
+ struct bpf_map **maps;
+ u32 i, n = 0;
+ int fd, err;
+
+ *mapsp = NULL;
+ *cntp = 0;
+
+ if (!attr->fd_array || !attr->fd_array_cnt)
+ return 0;
+ /*
+ * Stricter than the verifier, which dedups fd_array entries against
+ * used_maps: every entry here is folded into the signed data
+ * individually, so cap the raw count.
+ */
+ if (attr->fd_array_cnt > MAX_USED_MAPS)
+ return -E2BIG;
+
+ maps = kcalloc(attr->fd_array_cnt, sizeof(*maps), GFP_KERNEL);
+ if (!maps)
+ return -ENOMEM;
+
+ for (i = 0; i < attr->fd_array_cnt; i++) {
+ struct bpf_map *map;
+
+ if (copy_from_bpfptr_offset(&fd, fds, i * sizeof(int),
+ sizeof(int))) {
+ err = -EFAULT;
+ goto err_put;
+ }
+ map = bpf_map_get(fd);
+ if (IS_ERR(map)) {
+ err = PTR_ERR(map);
+ goto err_put;
+ }
+ if (!map->excl_prog_sha) {
+ bpf_map_put(map);
+ err = -EINVAL;
+ goto err_put;
+ }
+ maps[n++] = map;
+ }
+
+ *mapsp = maps;
+ *cntp = n;
+ return 0;
+err_put:
+ bpf_prog_put_excl_maps(maps, n);
+ return err;
+}
+
static int bpf_prog_verify_signature(struct bpf_prog *prog, union bpf_attr *attr,
- bool is_kernel, s32 *keyring_serial)
+ bool is_kernel, s32 *keyring_serial,
+ struct bpf_map **excl_maps, u32 excl_cnt)
{
bpfptr_t usig = make_bpfptr(attr->signature, is_kernel);
- struct bpf_dynptr_kern sig_ptr, insns_ptr;
+ struct bpf_dynptr_kern sig_ptr, data_ptr;
struct bpf_key *key = NULL;
- void *sig;
+ void *sig, *data = NULL;
+ u32 i, off, insns_sz;
+ u64 data_sz;
int err = 0;
/*
@@ -2925,20 +2993,78 @@ static int bpf_prog_verify_signature(struct bpf_prog *prog, union bpf_attr *attr
return PTR_ERR(sig);
}
+ insns_sz = prog->len * sizeof(struct bpf_insn);
+ data_sz = insns_sz;
+ for (i = 0; i < excl_cnt; i++) {
+ if (!READ_ONCE(excl_maps[i]->frozen) ||
+ !excl_maps[i]->ops->map_direct_value_addr) {
+ err = -EPERM;
+ goto out;
+ }
+ data_sz += excl_maps[i]->value_size;
+ }
+
+ if (bpf_dynptr_check_size(data_sz)) {
+ err = -E2BIG;
+ goto out;
+ }
+ data = kvmalloc(data_sz, GFP_KERNEL);
+ if (!data) {
+ err = -ENOMEM;
+ goto out;
+ }
+ memcpy(data, prog->insnsi, insns_sz);
+ off = insns_sz;
+ for (i = 0; i < excl_cnt; i++) {
+ struct bpf_map *map = excl_maps[i];
+ u64 addr;
+
+ err = map->ops->map_direct_value_addr(map, &addr, 0);
+ if (err)
+ goto out;
+ memcpy(data + off, (void *)(unsigned long)addr, map->value_size);
+ off += map->value_size;
+ }
+
+ bpf_dynptr_init(&data_ptr, data, BPF_DYNPTR_TYPE_LOCAL, 0, data_sz);
bpf_dynptr_init(&sig_ptr, sig, BPF_DYNPTR_TYPE_LOCAL, 0,
attr->signature_size);
- bpf_dynptr_init(&insns_ptr, prog->insnsi, BPF_DYNPTR_TYPE_LOCAL, 0,
- prog->len * sizeof(struct bpf_insn));
- err = bpf_verify_pkcs7_signature((struct bpf_dynptr *)&insns_ptr,
+ err = bpf_verify_pkcs7_signature((struct bpf_dynptr *)&data_ptr,
(struct bpf_dynptr *)&sig_ptr, key);
if (!err)
*keyring_serial = bpf_key_serial(key);
+out:
+ kvfree(data);
bpf_key_put(key);
kvfree(sig);
return err;
}
+static int bpf_prog_check_excl_used_maps(struct bpf_prog *prog,
+ struct bpf_map **excl_maps, u32 excl_cnt)
+{
+ u32 i, j;
+
+ for (i = 0; i < prog->aux->used_map_cnt; i++) {
+ struct bpf_map *map = prog->aux->used_maps[i];
+ bool folded = false;
+
+ if (!map->excl_prog_sha)
+ continue;
+ for (j = 0; j < excl_cnt; j++) {
+ if (excl_maps[j] == map) {
+ folded = true;
+ break;
+ }
+ }
+ if (!folded)
+ return -EACCES;
+ }
+
+ return 0;
+}
+
static int bpf_prog_mark_insn_arrays_ready(struct bpf_prog *prog)
{
int err;
@@ -2968,8 +3094,10 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, struct bpf_log_at
{
enum bpf_prog_type type = attr->prog_type;
struct bpf_prog *prog, *dst_prog = NULL;
+ struct bpf_map **excl_maps = NULL;
struct btf *attach_btf = NULL;
struct bpf_token *token = NULL;
+ u32 excl_cnt = 0;
bool bpf_cap;
int err;
char license[128];
@@ -3129,10 +3257,17 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, struct bpf_log_at
/* eBPF programs must be GPL compatible to use GPL-ed functions */
prog->gpl_compatible = license_is_gpl_compatible(license) ? 1 : 0;
if (attr->signature) {
+ err = bpf_prog_collect_excl_maps(attr, uattr.is_kernel,
+ &excl_maps, &excl_cnt);
+ if (err)
+ goto free_prog;
+
err = bpf_prog_verify_signature(prog, attr, uattr.is_kernel,
- &prog->aux->sig.keyring_serial);
+ &prog->aux->sig.keyring_serial,
+ excl_maps, excl_cnt);
if (err)
goto free_prog;
+
prog->aux->sig.keyring_type = bpf_classify_keyring(attr->keyring_id);
prog->aux->sig.verdict = BPF_SIG_VERIFIED;
} else {
@@ -3187,12 +3322,25 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, struct bpf_log_at
err = security_bpf_prog_load(prog, attr, token, uattr.is_kernel);
if (err)
goto free_prog;
+ if (excl_cnt) {
+ err = bpf_prog_calc_tag(prog);
+ if (err < 0)
+ goto free_prog;
+ }
/* run eBPF verifier */
err = bpf_check(&prog, attr, uattr, attr_log);
if (err < 0)
goto free_used_maps;
+ if (prog->aux->sig.verdict == BPF_SIG_VERIFIED) {
+ err = bpf_prog_check_excl_used_maps(prog, excl_maps, excl_cnt);
+ if (err < 0)
+ goto free_used_maps;
+ }
+ bpf_prog_put_excl_maps(excl_maps, excl_cnt);
+ excl_maps = NULL;
+
err = bpf_prog_mark_insn_arrays_ready(prog);
if (err < 0)
goto free_used_maps;
@@ -3225,6 +3373,7 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, struct bpf_log_at
return err;
free_used_maps:
+ bpf_prog_put_excl_maps(excl_maps, excl_cnt);
/* In case we have subprogs, we need to wait for a grace
* period before we can tear down JIT memory since symbols
* are already exposed under kallsyms.
@@ -3233,6 +3382,7 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, struct bpf_log_at
return err;
free_prog:
+ bpf_prog_put_excl_maps(excl_maps, excl_cnt);
free_uid(prog->aux->user);
if (prog->aux->attach_btf)
btf_put(prog->aux->attach_btf);
--
2.43.0
next prev parent reply other threads:[~2026-06-10 23:03 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-10 23:03 [PATCH bpf-next 0/5] Verify BPF signed loader at load time Daniel Borkmann
2026-06-10 23:03 ` Daniel Borkmann [this message]
2026-06-10 23:03 ` [PATCH bpf-next 2/5] libbpf: Drop in-loader metadata check for load-time verification Daniel Borkmann
2026-06-10 23:03 ` [PATCH bpf-next 3/5] bpftool: Cover loader metadata with the program signature Daniel Borkmann
2026-06-10 23:48 ` bot+bpf-ci
2026-06-10 23:03 ` [PATCH bpf-next 4/5] selftests/bpf: Verify load-time signed loader metadata Daniel Borkmann
2026-06-10 23:03 ` [PATCH bpf-next 5/5] Documentation/bpf: Add BPF signing and enforcement doc Daniel Borkmann
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=20260610230329.727075-2-daniel@iogearbox.net \
--to=daniel@iogearbox.net \
--cc=James.Bottomley@hansenpartnership.com \
--cc=ast@kernel.org \
--cc=bboscaccy@linux.microsoft.com \
--cc=bpf@vger.kernel.org \
--cc=kpsingh@kernel.org \
--cc=linux-security-module@vger.kernel.org \
--cc=memxor@gmail.com \
--cc=paul@paul-moore.com \
--cc=torvalds@linux-foundation.org \
/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