[PATCH bpf-next v3 2/6] bpf: Verify signed loader metadata at load time

Paul Moore paul at paul-moore.com
Thu Jul 2 22:05:17 UTC 2026


On Thu, Jul 2, 2026 at 10:36 AM Daniel Borkmann <daniel at iogearbox.net> 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 at 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]
> ---
>  include/linux/bpf_verifier.h |   1 +
>  kernel/bpf/syscall.c         |  76 +--------------
>  kernel/bpf/verifier.c        | 175 +++++++++++++++++++++++++++++++++++
>  3 files changed, 178 insertions(+), 74 deletions(-)

...

> 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/

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) <paul at paul-moore.com>

> 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



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