[PATCH bpf-next v2 1/5] bpf: Verify signed loader metadata at load time
Paul Moore
paul at paul-moore.com
Wed Jun 24 15:12:15 UTC 2026
On Wed, Jun 24, 2026 at 10:03 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.
>
> 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 | 163 ++++++++++++++++++++++++++++++++++-
> 3 files changed, 165 insertions(+), 75 deletions(-)
...
> diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
> index b44106c8ea75..026b61d78bdb 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)
We must preserve the existing location of the call into the
security_bpf_prog_load() hook as some users rely on this hook being
called *before* the verifier runs.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 2abc79dbf281..9cd2b62da380 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -19758,11 +19895,28 @@ 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->check_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->check_signature) {
> + ret = bpf_prog_verify_signature(env, attr, uattr.is_kernel);
> + if (ret)
> + goto skip_full_check;
> + signed_map_cnt = env->used_map_cnt;
> + }
> +
> + ret = security_bpf_prog_load(env->prog, attr, env->prog->aux->token,
> + uattr.is_kernel);
> + if (ret)
> + goto skip_full_check;
We can always create a new LSM hook for this call site, e.g.
security_bpf_prog_verify_signature(...).
> mark_verifier_state_clean(env);
>
> if (IS_ERR(btf_vmlinux)) {
--
paul-moore.com
More information about the Linux-security-module-archive
mailing list