[PATCH bpf-next v2 1/5] bpf: Verify signed loader metadata at load time
Daniel Borkmann
daniel at iogearbox.net
Thu Jun 25 20:37:34 UTC 2026
On 6/24/26 8:42 PM, Paul Moore wrote:
> On Wed, Jun 24, 2026 at 11:37 AM Daniel Borkmann <daniel at iogearbox.net> wrote:
>> On 6/24/26 5:12 PM, Paul Moore wrote:
>>> On Wed, Jun 24, 2026 at 10:03 AM Daniel Borkmann <daniel at iogearbox.net> wrote:
>> [...]
>>>> 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.
>>
>> Keep in mind that the verifier /at this point/ of the new location did
>> _not_ verify anything. So there is no heavy-duty work happening yet at
>> security_bpf_prog_load. The work that is done before security_bpf_prog_load
>> is basically setting up the env, initializing the verifier log, and doing
>> the process_fd_array which is resolving the map/BTF objects. But it did
>> not walk any instructions etc, so semantics of the security_bpf_prog_load
>> hook did not change from a user PoV.
>
> There is still a reasonable amount of work between the existing and
> new call sites, and the existing location outside of bpf_check()
> offers an additional robustness benefit that future verifier changes
> are less likely to impact the hook. If I'm completely honest, I also
> need to consider the events of the past year and a half; I'm now much
> less inclined to support LSM hook changes in the BPF subsystem because
> I'm very concerned about our ability to revert/modify those changes in
> the future if needed. That doesn't mean I won't support LSM hook
> changes in BPF, but such changes are going to need to have a *very*
> strong advantage from a LSM perspective to offset the risk associated
> with the current BPF subsystem.
From where you sit with regards to LSMs that is a natural stance towards
all kernel code, but coming back to the LSM hook, to me this is way too
excessive that we should add *yet another* LSM hook. So, just for loading
a *single* BPF program we would then need to pass through *four* layers of
LSM hooks:
1) security_bpf (cmd=PROG_LOAD): for gating various bpf subcmds
2) security_bpf_prog_load: historical admission hook (CAP/token,
prog_type, attach point), pre-verification
3) security_bpf_prog_verify_signature: newly asked admission hook,
same role as 2), plus the BPF signature verdict
4) security_bpf_prog: gate handing the prog fd back to userspace,
verification done & signature verified
The use-cases of 2) and 3) conflate. I strongly prefer to just keep a
total of 3 LSM hooks (as-is today): 3) makes 2) incoherent given they
are the /same class/ of hook, that is, access-control admission on the
load and split only by _what_ they can see. Worse, with the split, for
a signed BPF program security_bpf_prog_load 2) admits a program whose
signature has not been checked, so a policy gating at 2) is structurally
unable to express "admit only verified" and every such policy is forced
onto 3) *anyway*. In other words, you don't get two complementary hooks,
but rather, you get one real admission hook aka 3) plus a now-degraded
/legacy/ hook 2) that can't answer the question operators actually want
to ask. So, no, we're not adding yet another LSM hook.
> Based on what I see in this patchset, the security_bpf_prog_load()
> call should remain in the current location. If you need an additional
> hook after the bpf_prog_verify_signature() call I'm happy to work with
> you on that.
See above.
> I also have to bring up the same question I asked back in your v1
> posting: have you discussed this signature approach with Alexei? Your
> patches abandon and remove KP's signature scheme in favor of what is
> effectively Blaise's signature scheme from last fall; Alexei argued
> very strongly against these changes in the past. I'd hate to spend a
> lot more time reviewing and discussing patches that Alexei is simply
> going to NACK once again.
I think last time I already stated that this is not "effectively Blaise's
signature scheme" for couple of reasons: i) we sign over the raw bytes, not
the derived hash anymore, so the hashing is only used in the context to tie
the map to the loader program, but not anymore for the signature. ii) its one
/single scheme/ and not a parallel branch, so the main loader is built upon
the updated signing scheme rather than having this as an option on the side;
in other words, this replaces the in-loader check and there's a single
PKCS#7-over-bytes path, not an 'if (signature_maps_size)' fork; and iii)
given we expose the verification result in the BPF prog, we also don't need
a new LSM hook and can just piggy back on the existing security_bpf_prog
which also has the possibility to still reject late at this point.
From where you started out back then, it was the stance that while the
original KP approach generically addresses all the use cases for loading
BPF related to relocations via the lskel loader, Blaise proposed a parallel
scheme which would only allow static programs (only insns, no maps) which
is 1% of use cases it covers for the BPF ecosystem and users are stuck on
figuring out which approach they need to go with. So when I took over the
BTF series with the extra kfunc that KP proposed, I was mainly looking at
that series trying to figure out how we can get away without pulling in BTF
complexity for the signed loader and with a compromise that would potentially
satisfy all parties under a unified signature scheme and having the kernel
side (rather than loader side) providing the hard guarantee. So I cannot
directly speak for Alexei/KP, but I think this proposal should satisfy all
parties under one roof. I've build out user space tooling in addition to
test real world BPF program to make sure they work in combination with maps
and BTF under this scheme as well as map-less BPF programs.
>>>> 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)) {
>
More information about the Linux-security-module-archive
mailing list