Re: [PATCH bpf-next v4 3/9] bpf: Verify signed loader metadata at load time
Paul Moore <[email protected]>
| Newsgroups | org.kernel.vger.linux-security-module,org.kernel.vger.bpf |
|---|---|
| Message-ID | <CAHC9VhRWG=Of4pKmo8KRjCYnVmfX5ZD7JPVa0D0N0Z=1ts4iQw@mail.gmail.com> |
On Mon, Jul 6, 2026 at 9:56 AM Daniel Borkmann <[email protected]> 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. 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 [0][1]. > 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 [2]. Covering > both the loader and its maps directly with the PKCS#7 signature is what > Blaise Boscaccy's patchsets proposed in several forms. Let's implement > the former, without growing the UAPI, and in particular as a single > unified scheme where the signature spans the raw bytes rather than > derived hashes. > > 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 by the signature path; for them the > LSM admission hook merely moves below fd_array resolution, with minimal > bounded work in between (see the previous commit). 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. > > Finally, some remarks around the security_bpf_prog_load() placement > given there was pushback, demanding either a new hook or a reuse of > the existing security_bpf_prog() hook [3]: Just for loading a single > BPF program it 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, thus BPF community prefers 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, one doesn't get two complementary hooks, > but rather, one real admission hook aka 3) plus a now-degraded /legacy/ > hook 2) that can't answer the question operators actually want to ask. > > Reusing security_bpf_prog() 4) for admission is no alternative either: > it fires only after the entire verifier (and JIT) pipeline ran, so > denying a not-yet-verified program at that point burns exactly the > work a denial is supposed to avoid, and by then the program has an id > assigned and the kallsyms/perf/audit load events fired. Policies are > free to also consume the signature verdict at 4), but admission control > belongs into security_bpf_prog_load(). Hence the latter remains the only > admission hook, merely moved past signature verification; with moving > large allocations further down into the BPF verifier, there is now only > minimal work between the old and new location. See my comment on patch 0/9, but if we can combine this patch with the vzalloc(progam) patch, I'll be happy to drop my NACK, and you can trim the last half of this patch description ;) I suspect that squashing the two patches should also quiet sashiko's comments as it will be able to see the vzalloc() below the security_bpf_prog_load() hook in this same patch. > Signed-off-by: Daniel Borkmann <[email protected]> > Nacked-by: Paul Moore <[email protected]> # (don't move bpf_prog_load LSM hook) > Link: https://lore.kernel.org/bpf/2f71d6c03698eb17d51f7247efde777627ee578a.camel@HansenPartnership.com [0] > Link: https://lore.kernel.org/lkml/ecf0521ed302db672672ebfbc670ecfba36a6e00.camel@HansenPartnership.com [1] > Link: https://lore.kernel.org/bpf/88703f00d5b7a779728451008626efa45e42db3d.camel@HansenPartnership.com [2] > Link: https://lore.kernel.org/bpf/[email protected] [3] > --- > include/linux/bpf_verifier.h | 1 + > kernel/bpf/syscall.c | 76 +-------------- > kernel/bpf/verifier.c | 175 +++++++++++++++++++++++++++++++++++ > 3 files changed, 178 insertions(+), 74 deletions(-) -- paul-moore.com