Re: [PATCH bpf-next v3 2/6] bpf: Verify signed loader metadata at load time
From: Paul Moore <paul@paul-moore.com>
Date: 2026-07-02 22:05:29
Also in:
bpf
On Thu, Jul 2, 2026 at 10:36 AM Daniel Borkmann [off-list ref] wrote:
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.
While you've documented some of the objections from the LSM side, you've missed the largest objection to the existing BPF signing scheme: KP's scheme runs the PKCS7 signature over the lskel loader, while the scheme that Blaise proposed (in multiple forms, not just Hornet), runs the PKCS7 signature over both the lskel loader and the associated maps, just as you are doing in this patchset. Moving towards a scheme where the PKCS7 signature encompasses both the lskel loader and the maps is a good thing, I'm happy to see that. However, I do have other issues with this patch that I've mentioned previously (more below).
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_check(), once the fd_array maps are resolved into used_maps, before the LSM admission hook and the rest of verification. A program is either BPF_SIG_UNSIGNED or BPF_SIG_VERIFIED, with nothing in between. While folding 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_check(), 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 (local) [0] Link: https://lore.kernel.org/bpf/2f71d6c03698eb17d51f7247efde777627ee578a.camel@HansenPartnership.com (local) [1] Link: https://lore.kernel.org/lkml/ecf0521ed302db672672ebfbc670ecfba36a6e00.camel@HansenPartnership.com (local) [2] Link: https://lore.kernel.org/bpf/88703f00d5b7a779728451008626efa45e42db3d.camel@HansenPartnership.com (local) [3] --- include/linux/bpf_verifier.h | 1 + kernel/bpf/syscall.c | 76 +-------------- kernel/bpf/verifier.c | 175 +++++++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 74 deletions(-)
...
quoted hunk ↗ jump to hunk
diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c index 6db306d23b47..e898fad01aaf 100644 --- a/kernel/bpf/syscall.c +++ b/kernel/bpf/syscall.c@@ -3189,10 +3121,6 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, struct bpf_log_at if (err < 0) goto free_prog; - err = security_bpf_prog_load(prog, attr, token, uattr.is_kernel); - if (err) - goto free_prog; - /* run eBPF verifier */ err = bpf_check(&prog, attr, uattr, attr_log); if (err < 0)
As I mentioned previously, the security_bpf_prog_load() hook should not be moved. Its placement was deliberate, and moving it as you've done in this patch creates a security regression. Without this patch, for processes where SELinux is preventing BPF programs from being loaded, it is able to do so before the process allocates a potentially very large chunk of kernel memory, up to one million BPF instructions. With this patch, even in cases where SELinux will prevent the BPF program load operation, it is not able to do so before the process triggers a potential sizable memory allocation, opening the door for a local DoS attack. In an effort to work with you on this, I did bring up two alternative solutions: a new LSM hook for the signature verification, or the use of the existing security_bpf_prog() hook. The latter option is conceptually less clean than the former, but it should still work to control access to the BPF program based on the signature results. You've mentioned that you are not in favor of adding a new hook, but you have ignored the other potential solution of using the security_bpf_prog() hook. You can (re)read my earlier comments at the lore link below: https://lore.kernel.org/linux-security-module/CAHC9VhTtaT_8AXSfNKiq4cGq7T=6Dh=huM8niBCwOP8kj+ugjQ@mail.gmail.com/ (local) While I don't like to do this, you haven't given me much of a choice; the security regression can not be ignored: Nacked-by: Paul Moore (don't move bpf_prog_load LSM hook) [off-list ref]
quoted hunk ↗ jump to hunk
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index 44bb6ce17a1c..1609053a0eeb 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c@@ -19901,11 +20060,27 @@ int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr, ret = bpf_vlog_init(&env->log, attr_log->level, attr_log->ubuf, attr_log->size); if (ret) goto err_unlock; + if (env->signature) { + ret = bpf_prog_calc_tag(env->prog); + if (ret < 0) + goto skip_full_check; + } ret = process_fd_array(env, attr, uattr); if (ret) goto skip_full_check; + if (env->signature) { + ret = bpf_prog_verify_signature(env, attr, uattr.is_kernel); + if (ret) + goto skip_full_check; + } + + ret = security_bpf_prog_load(env->prog, attr, env->prog->aux->token, + uattr.is_kernel); + if (ret) + goto skip_full_check; + mark_verifier_state_clean(env); if (IS_ERR(btf_vmlinux)) { --2.43.0
-- paul-moore.com