[PATCH 00/12] Signed BPF programs

Blaise Boscaccy bboscaccy at linux.microsoft.com
Tue Jul 8 15:15:39 UTC 2025


KP Singh <kpsingh at kernel.org> writes:

> BPF Signing has gone over multiple discussions in various conferences with the
> kernel and BPF community and the following patch series is a culmination
> of the current of discussion on signed BPF programs. Once signing is
> implemented, the next focus would be to implement the right security policies
> for all BPF use-cases (dynamically generated bpf programs, simple non CO-RE
> programs).
>
> Signing also paves the way for allowing unrivileged users to
> load vetted BPF programs and helps in adhering to the principle of least
> privlege by avoiding unnecessary elevation of privileges to CAP_BPF and
> CAP_SYS_ADMIN (ofcourse, with the appropriate security policy active).
>
> A early version of this design was proposed in [1]:
>
> # General Idea: Trusted Hash Chain
>
> The key idea of the design is to use a signing algorithm that allows
> us to integrity-protect a number of future payloads, including their
> order, by creating a chain of trust.
>
> Consider that Alice needs to send messages M_1, M_2, ..., M_n to Bob.
> We define blocks of data such that:
>
>     B_n = M_n || H(termination_marker)
>
> (Each block contains its corresponding message and the hash of the
> *next* block in the chain.)
>
>     B_{n-1} = M_{n-1} || H(B_n)
>     B_{n-2} = M_{n-2} || H(B_{n-1})
>
>   ...
>
>     B_2 = M_2 || H(B_3)
>     B_1 = M_1 || H(B_2)
>
> Alice does the following (e.g., on a build system where all payloads
> are available):
>
>   * Assembles the blocks B_1, B_2, ..., B_n.
>   * Calculates H(B_1) and signs it, yielding Sig(H(B_1)).
>
> Alice sends the following to Bob:
>
>     M_1, H(B_2), Sig(H(B_1))
>
> Bob receives this payload and does the following:
>
>     * Reconstructs B_1 as B_1' using the received M_1 and H(B_2)
> (i.e., B_1' = M_1 || H(B_2)).
>     * Recomputes H(B_1') and verifies the signature against the
> received Sig(H(B_1)).
>     * If the signature verifies, it establishes the integrity of M_1
> and H(B_2) (and transitively, the integrity of the entire chain). Bob
> now stores the verified H(B_2) until it receives the next message.
>     * When Bob receives M_2 (and H(B_3) if n > 2), it reconstructs
> B_2' (e.g., B_2' = M_2 || H(B_3), or if n=2, B_2' = M_2 ||
> H(termination_marker)). Bob then computes H(B_2') and compares it
> against the stored H(B_2) that was verified in the previous step.
>
> This process continues until the last block is received and verified.
>
> Now, applying this to the BPF signing use-case, we simplify to two messages:
>
>     M_1 = I_loader (the instructions of the loader program)
>     M_2 = M_metadata (the metadata for the loader program, passed in a
> map, which includes the programs to be loaded and other context)
>
> For this specific BPF case, we will directly sign a composite of the
> first message and the hash of the second. Let H_meta = H(M_metadata).
> The block to be signed is effectively:
>
>     B_signed = I_loader || H_meta
>
> The signature generated is Sig(B_signed).
>
> The process then follows a similar pattern to the Alice and Bob model,
> where the kernel (Bob) verifies I_loader and H_meta using the
> signature. Then, the trusted I_loader is responsible for verifying
> M_metadata against the trusted H_meta.
>
>>From an implementation standpoint:
>
> # Build
>
> bpftool (or some other tool in a trusted build environment) knows
> about the metadata (M_metadata) and the loader program (I_loader). It
> first calculates H_meta = H(M_metadata). Then it constructs the object
> to be signed and computes the signature:
>
>     Sig(I_loader || H_meta)
>
> # Loader
>
> The loader program and the metadata are a hermetic representation of the source
> of the eBPF program, its maps and context. The loader program is generated by
> libbpf as a part of a standard API i.e. bpf_object__gen_loader.
>
> ## Supply chain
>
> While users can use light skeletons as a convenient method to use signing
> support, they can directly use the loader program generation using libbpf
> (bpf_object__gen_loader) into their own trusted toolchains.
>
> libbpf, which has access to the program's instruction buffer is a key part of
> the TCB of the build environment
>
> An advanced threat model that does not intend to depend on libbpf (or any provenant
> userspace BPF libraries) due to supply chain risks despite it being developed
> in the kernel source and by the kernel community will require reimplmenting a
> lot of the core BPF userspace support (like instruction relocation, map handling).
>
> Such an advanced user would also need to integrate the generation of the loader
> into their toolchain.
>
> Given that many use-cases (e.g. Cilium) generate trusted BPF programs,
> trusted loaders are an inevitability and a requirement for signing support, a
> entrusting loader programs will be a fundamental requirement for an security
> policy.
>
> The initial instructions of the loader program verify the SHA256 hash
> of the metadata (M_metadata) that will be passed in a map. These instructions
> effectively embed the precomputed H_meta as immediate values.
>
>     ld_imm64 r1, const_ptr_to_map // insn[0].src_reg == BPF_PSEUDO_MAP_IDX
>     r2 = *(u64 *)(r1 + 0);
>     ld_imm64 r3, sha256_of_map_part1 // precomputed by bpf_object__gen_load/libbpf (H_meta_1)
>     if r2 != r3 goto out;
>
>     r2 = *(u64 *)(r1 + 8);
>     ld_imm64 r3, sha256_of_map_part2 // precomputed by bpf_object__gen_load/libbpf (H_meta_2)
>     if r2 != r3 goto out;
>
>     r2 = *(u64 *)(r1 + 16);
>     ld_imm64 r3, sha256_of_map_part3 // precomputed by bpf_object__gen_load/libbpf (H_meta_3)
>     if r2 != r3 goto out;
>
>     r2 = *(u64 *)(r1 + 24);
>     ld_imm64 r3, sha256_of_map_part4 // precomputed by bpf_object__gen_load/libbpf (H_meta_4)
>     if r2 != r3 goto out;
>     ...
>
> This implicitly makes the payload equivalent to the signed block (B_signed)
>
>     I_loader || H_meta
>
> bpftool then generates the signature of this I_loader payload (which
> now contains the expected H_meta) using a key and an identity:
>
> This signature is stored in bpf_attr, which is extended as follows for
> the BPF_PROG_LOAD command:
>
>     __aligned_u64 signature;
>     __u32 signature_size;
>     __u32 keyring_id;
>
> The reasons for a simpler UAPI is that it's more future proof (e.g.) with more
> stable instruction buffers, loader programs being directly into the compilers.
> A simple API also allows simple programs e.g. for networking that don't need
> loader programs to directly use signing.
>
> # Extending OBJ_GET_INFO_BY_FD for hashes
>
> OBJ_GET_INFO_BY_FD is used to get information about BPF objects (maps, programs, links) and
> returning the hash of the map is a natural extension of the UAPI as it can be
> helpful for debugging, fingerprinting etc.
>
> Currently, it's only implemented for BPF_MAP_TYPE_ARRAY. It can be trivially
> extended for BPF programs to return the complete SHA256 along with the tag.
>
> The SHA is stored in struct bpf_map for exclusive and frozen maps
>
>     struct bpf_map {
>     +   u64 sha[4];
>         const struct bpf_map_ops *ops;
>         struct bpf_map *inner_map_meta;
>     };
>
> ## Exclusive BPF maps
>
> Exclusivity ensures that the map can only be used by a future BPF
> program whose SHA256 hash matches sha256_of_future_prog.
>
> First, bpf_prog_calc_tag() is updated to compute the SHA256 instead of
> SHA1, and this hash is stored in struct bpf_prog_aux:
>
>     @@ -1588,6 +1588,7 @@ struct bpf_prog_aux {
>          int cgroup_atype; /* enum cgroup_bpf_attach_type */
>          struct bpf_map *cgroup_storage[MAX_BPF_CGROUP_STORAGE_TYPE];
>          char name[BPF_OBJ_NAME_LEN];
>     +    u64 sha[4];
>          u64 (*bpf_exception_cb)(u64 cookie, u64 sp, u64 bp, u64, u64);
>          // ...
>     };
>
> An exclusive is created by passing an excl_prog_hash
> (and excl_prog_hash_size) in the BPF_MAP_CREATE command.
> When a BPF program is subsequently loaded and it attempts to use this map,
> the kernel will compare the program's own SHA256 hash against the one
> registered with the map, if matching, it will be added to prog->used_maps[].
>
> The program load will fail if the hashes do not match or if the map is
> already in use by another (non-matching) exclusive program.
>
> Exclusive maps ensure that no other BPF programs and compromise the intergity of
> the map post the signature verification.
>
> NOTE: Exclusive maps cannot be added as inner maps.
>
> # Light Skeleton Sequence (Userspace Example)
>
> 	err = map_fd = skel_map_create(BPF_MAP_TYPE_ARRAY, "__loader.map",
> 				       opts->excl_prog_hash,
> 				       opts->excl_prog_hash_sz, 4,
> 				       opts->data_sz, 1);
> 	err = skel_map_update_elem(map_fd, &key, opts->data, 0);
>
> 	err = skel_map_freeze(map_fd);
>
> 	// Kernel computes the hash of the map.
> 	err = skel_obj_get_info_by_fd(map_fd);
>
> 	memset(&attr, 0, prog_load_attr_sz);
> 	attr.prog_type = BPF_PROG_TYPE_SYSCALL;
> 	attr.insns = (long) opts->insns;
> 	attr.insn_cnt = opts->insns_sz / sizeof(struct bpf_insn);
> 	attr.signature = (long) opts->signature;
> 	attr.signature_size = opts->signature_sz;
> 	attr.keyring_id = opts->keyring_id;
> 	attr.license = (long) "Dual BSD/GPL";
>
> The kernel will:
>
>     * Compute the hash of the provided I_loader bytecode.
>     * Verify the signature against this computed hash.
>     * Check if the metadata map (now exclusive) is intended for this
>       program's hash.
>
> The signature check happens in BPF_PROG_LOAD before the security_bpf_prog
> LSM hook.
>
> This ensures that the loaded loader program (I_loader), including the
> embedded expected hash of the metadata (H_meta), is trusted.
> Since the loader program is now trusted, it can be entrusted to verify
> the actual metadata (M_metadata) read from the (now exclusive and
> frozen) map against the embedded (and trusted) H_meta. There is no
> Time-of-Check-Time-of-Use (TOCTOU) vulnerability here because:
>
>     * The signature covers the I_loader and its embedded H_meta.
>     * The metadata map M_metadata is frozen before the loader program is loaded
>       and associated with it.
>     * The map is made exclusive to the specific (signed and verified)
>       loader program.
>
> [1] https://lore.kernel.org/bpf/CACYkzJ6VQUExfyt0=-FmXz46GHJh3d=FXh5j4KfexcEFbHV-vg@mail.gmail.com/#t
>

Can we expect to see a v2 of this patchset sometime soon? We are
planning on submitting follow-up patchsets that build on this effort.

-blaise


>
> KP Singh (12):
>   bpf: Implement an internal helper for SHA256 hashing
>   bpf: Update the bpf_prog_calc_tag to use SHA256
>   bpf: Implement exclusive map creation
>   libbpf: Implement SHA256 internal helper
>   libbpf: Support exclusive map creation
>   selftests/bpf: Add tests for exclusive maps
>   bpf: Return hashes of maps in BPF_OBJ_GET_INFO_BY_FD
>   bpf: Implement signature verification for BPF programs
>   libbpf: Update light skeleton for signing
>   libbpf: Embed and verify the metadata hash in the loader
>   bpftool: Add support for signing BPF programs
>   selftests/bpf: Enable signature verification for all lskel tests
>
>  include/linux/bpf.h                           |  22 +-
>  include/linux/filter.h                        |   6 -
>  include/uapi/linux/bpf.h                      |  15 +-
>  kernel/bpf/arraymap.c                         |  17 ++
>  kernel/bpf/core.c                             |  88 ++++----
>  kernel/bpf/hashtab.c                          |  15 +-
>  kernel/bpf/syscall.c                          | 112 +++++++++-
>  kernel/bpf/verifier.c                         |   7 +
>  kernel/trace/bpf_trace.c                      |   6 +-
>  .../bpf/bpftool/Documentation/bpftool-gen.rst |  12 +
>  .../bpftool/Documentation/bpftool-prog.rst    |  12 +
>  tools/bpf/bpftool/Makefile                    |   6 +-
>  tools/bpf/bpftool/cgroup.c                    |   5 +-
>  tools/bpf/bpftool/gen.c                       |  58 ++++-
>  tools/bpf/bpftool/main.c                      |  21 +-
>  tools/bpf/bpftool/main.h                      |  11 +
>  tools/bpf/bpftool/prog.c                      |  25 +++
>  tools/bpf/bpftool/sign.c                      | 211 ++++++++++++++++++
>  tools/include/uapi/linux/bpf.h                |  15 +-
>  tools/lib/bpf/bpf.c                           |   6 +-
>  tools/lib/bpf/bpf.h                           |   4 +-
>  tools/lib/bpf/bpf_gen_internal.h              |   2 +
>  tools/lib/bpf/gen_loader.c                    |  52 +++++
>  tools/lib/bpf/libbpf.c                        | 125 ++++++++++-
>  tools/lib/bpf/libbpf.h                        |  16 +-
>  tools/lib/bpf/libbpf.map                      |   5 +
>  tools/lib/bpf/libbpf_internal.h               |   9 +
>  tools/lib/bpf/libbpf_version.h                |   2 +-
>  tools/lib/bpf/skel_internal.h                 |  57 ++++-
>  tools/testing/selftests/bpf/.gitignore        |   1 +
>  tools/testing/selftests/bpf/Makefile          |  13 +-
>  .../selftests/bpf/prog_tests/map_excl.c       | 130 +++++++++++
>  tools/testing/selftests/bpf/progs/map_excl.c  |  65 ++++++
>  tools/testing/selftests/bpf/test_progs.c      |  13 ++
>  34 files changed, 1079 insertions(+), 85 deletions(-)
>  create mode 100644 tools/bpf/bpftool/sign.c
>  create mode 100644 tools/testing/selftests/bpf/prog_tests/map_excl.c
>  create mode 100644 tools/testing/selftests/bpf/progs/map_excl.c
>
> -- 
> 2.43.0



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