bpf ci test#12436
Open
borkmann wants to merge 6 commits into
Open
Conversation
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_prog_load() before the LSM admission hook and before the verifier runs. A program is either BPF_SIG_UNSIGNED or BPF_SIG_VERIFIED, with nothing in between. While collecting the fd_array maps, a non-exclusive map bound to a signed program is rejected with -EINVAL, 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_prog_load(), 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 [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]
The signed gen_loader used to police its own metadata map from within BPF: emit_signature_match() read the kernel-cached map->sha[] back through hardcoded struct bpf_map offsets and compared it against a hash that compute_sha_update_offsets() baked into the signed instructions, after a BPF_OBJ_GET_INFO_BY_FD round-trip to populate map->sha[]. The kernel now verifies the metadata at BPF_PROG_LOAD time by folding the frozen contents of the loader's exclusive fd_array maps into the signature, so the loader no longer checks anything itself. Generated loaders thus carry no verification logic of their own anymore: Nothing in the signing chain depends on emitted loader bytecode doing the right thing. On the loading side, skel_internal.h now sets fd_array_cnt for a signed load so the kernel scans fd_array for the exclusive metadata map - still frozen, as the kernel requires - and the BPF_OBJ_GET_INFO_BY_FD round-trip to populate map->sha[] is gone. The struct bpf_map layout BUILD_BUG_ON()s on the kernel side are removed as well: they only pinned the ABI for the in-BPF read of map->sha[] that is no longer needed. Note: gen_hash is retained; it still marks a loader as signed so an untrusted host cannot re-dimension maps or override initial values now covered by the signature. Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
bpftool_prog_sign() signed only the loader instructions. The metadata blob the loader installs was left to an in-loader hash check, which the kernel now performs at load time over insns || metadata. Sign that same concatenation: pass the metadata blob (gen_loader_opts data) through to bpftool_prog_sign() and feed insns || metadata to CMS_final(). The excl_prog_hash stays a digest of the instructions alone; it binds the metadata map to the loader and is matched against prog->digest by the verifier, independent of what the signature covers. The signed artifact is now plain data: both bytes the signature covers are embedded verbatim in the generated skeleton, so signing and verifying an lskel is an ordinary CMS operation that a signer or auditor can perform (or reproduce) offline, without analyzing loader bytecode to establish what the signature actually attests to [0]. Signed-off-by: Daniel Borkmann <daniel@iogearbox.net> Link: https://lore.kernel.org/lkml/ecf0521ed302db672672ebfbc670ecfba36a6e00.camel@HansenPartnership.com [0]
The signed gen_loader no longer checks its metadata map from within BPF; the kernel does it at BPF_PROG_LOAD by folding the loader's frozen exclusive fd_array maps into the signature. Exercise that path end to end. Extend with more test cases (e.g. map-less program, asserting the LSM admission hook observes BPF_SIG_UNSIGNED and BPF_SIG_VERIFIED), and retire the subtests that asserted the old in-loader check, which no longer exists. # LDLIBS=-static PKG_CONFIG='pkg-config --static' ./vmtest.sh -- ./test_progs -t signed_loader [...] [ 1.842848] clocksource: Switched to clocksource tsc kernel-patches#409/1 signed_loader/loadtime_no_map:OK kernel-patches#409/2 signed_loader/loadtime_with_map:OK kernel-patches#409/3 signed_loader/metadata_match:OK kernel-patches#409/4 signed_loader/signature_enforced:OK kernel-patches#409/5 signed_loader/signed_nonexcl_fd_array_rejected:OK kernel-patches#409/6 signed_loader/signature_too_large:OK kernel-patches#409/7 signed_loader/signature_bad_keyring:OK kernel-patches#409/8 signed_loader/metadata_ctx_max_entries_ignored:OK kernel-patches#409/9 signed_loader/metadata_ctx_initial_value_ignored:OK kernel-patches#409/10 signed_loader/signature_authenticates_insns:OK kernel-patches#409/11 signed_loader/hash_requires_frozen:OK kernel-patches#409/12 signed_loader/no_update_after_freeze:OK kernel-patches#409/13 signed_loader/freeze_writable_mmap:OK kernel-patches#409/14 signed_loader/no_writable_mmap_frozen:OK kernel-patches#409/15 signed_loader/map_hash_matches_libbpf:OK kernel-patches#409/16 signed_loader/map_hash_multi_element:OK kernel-patches#409/17 signed_loader/map_hash_bad_size:OK kernel-patches#409/18 signed_loader/map_hash_unsupported_type:OK kernel-patches#409/19 signed_loader/lsm_signature_verdict:OK kernel-patches#409 signed_loader:OK Summary: 1/19 PASSED, 0 SKIPPED, 0 FAILED Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Describe the BPF signing design end to end: why a trusted loader is
needed, the signature(insns || metadata) contract, load-time
verification via fd_array (exclusive + frozen maps), the binary
BPF_SIG_{UNSIGNED,VERIFIED} verdict, and how [BPF] LSMs can enforce
policy on it.
This writes down the contract that the discussions with the LSM /
integrity folks converged on [0][1]: by the time security_bpf_prog_load()
is called, signature verification has fully completed and covers the
instructions plus the frozen contents of every bound exclusive map;
there is no intermediate "loader verified, payload pending" state to
reason about; and what BPF_SIG_VERIFIED means at each hook is spelled
out explicitly, including the post-verifier coverage check that keeps
the verdict binary.
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Link: https://lore.kernel.org/bpf/bc823ddbaf63e0e177eb46d1cc15076e4e2e689d.camel@HansenPartnership.com [0]
Link: https://lore.kernel.org/bpf/CAHC9VhSDkwGgPfrBUh7EgBKEJj_JjnY68c0YAmuuLT_i--GskQ@mail.gmail.com [1]
d9ed865 to
1b36123
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.