ser...@kernel.org writes: > On Fri, Mar 21, 2025 at 09:45:03AM -0700, Blaise Boscaccy wrote: >> This adds the Hornet Linux Security Module which provides signature >> verification of eBPF programs. >> >> Hornet uses a similar signature verification scheme similar to that of > > used 'similar' twice > >> kernel modules. A pkcs#7 signature is appended to the end of an >> executable file. During an invocation of bpf_prog_load, the signature >> is fetched from the current task's executable file. That signature is >> used to verify the integrity of the bpf instructions and maps which >> where passed into the kernel. Additionally, Hornet implicitly trusts any > > s/where/were > >> programs which where loaded from inside kernel rather than userspace, > > s/where/were > >> which allows BPF_PRELOAD programs along with outputs for BPF_SYSCALL >> programs to run. >> >> Hornet allows users to continue to maintain an invariant that all code >> running inside of the kernel has been signed and works well with >> light-skeleton based loaders, or any statically generated program that >> doesn't require userspace instruction rewriting. >> >> Signed-off-by: Blaise Boscaccy <bbosca...@linux.microsoft.com> >> --- >> Documentation/admin-guide/LSM/Hornet.rst | 51 +++++ >> crypto/asymmetric_keys/pkcs7_verify.c | 10 + >> include/linux/kernel_read_file.h | 1 + >> include/linux/verification.h | 1 + >> include/uapi/linux/lsm.h | 1 + >> security/Kconfig | 3 +- >> security/Makefile | 1 + >> security/hornet/Kconfig | 11 ++ >> security/hornet/Makefile | 4 + >> security/hornet/hornet_lsm.c | 239 +++++++++++++++++++++++ >> 10 files changed, 321 insertions(+), 1 deletion(-) >> create mode 100644 Documentation/admin-guide/LSM/Hornet.rst >> create mode 100644 security/hornet/Kconfig >> create mode 100644 security/hornet/Makefile >> create mode 100644 security/hornet/hornet_lsm.c >> >> diff --git a/Documentation/admin-guide/LSM/Hornet.rst >> b/Documentation/admin-guide/LSM/Hornet.rst >> new file mode 100644 >> index 0000000000000..fa112412638f1 >> --- /dev/null >> +++ b/Documentation/admin-guide/LSM/Hornet.rst >> @@ -0,0 +1,51 @@ >> +====== >> +Hornet >> +====== >> + >> +Hornet is a Linux Security Module that provides signature verification >> +for eBPF programs. This is selectable at build-time with >> +``CONFIG_SECURITY_HORNET``. >> + >> +Overview >> +======== >> + >> +Hornet provides signature verification for eBPF programs by utilizing >> +the existing PKCS#7 infrastructure that's used for module signature >> +verification. Hornet works by creating a buffer containing the eBPF >> +program instructions along with its associated maps and checking a >> +signature against that buffer. The signature is appended to the end of >> +the lskel executable file and is extracted at runtime via >> +get_task_exe_file. Hornet works by hooking into the >> +security_bpf_prog_load hook. Load invocations that originate from the >> +kernel (bpf preload, results of bpf_syscall programs, etc.) are >> +allowed to run unconditionally. Calls that originate from userspace >> +require signature verification. If signature verification fails, the >> +program will fail to load. >> + >> +Instruction/Map Ordering >> +======================== >> + >> +Hornet supports both sparse-array based maps via map discovery along >> +with the newly added fd_array_cnt API for continuous map arrays. The >> +buffer used for signature verification is assumed to be the >> +instructions followed by all maps used, ordered by their index in >> +fd_array. >> + >> +Tooling >> +======= >> + >> +Some tooling is provided to aid with the development of signed eBPF lskels. >> + >> +extract-skel.sh >> +--------------- >> + >> +This simple shell script extracts the instructions and map data used >> +by the light skeleton from the autogenerated header file created by >> +bpftool. >> + >> +sign-ebpf >> +--------- >> + >> +sign-ebpf works similarly to the sign-file script with one key >> +difference: it takes a separate input binary used for signature >> +verification and will append the signature to a different output file. >> diff --git a/crypto/asymmetric_keys/pkcs7_verify.c >> b/crypto/asymmetric_keys/pkcs7_verify.c >> index f0d4ff3c20a83..1a5fbb3612188 100644 >> --- a/crypto/asymmetric_keys/pkcs7_verify.c >> +++ b/crypto/asymmetric_keys/pkcs7_verify.c >> @@ -428,6 +428,16 @@ int pkcs7_verify(struct pkcs7_message *pkcs7, >> } >> /* Authattr presence checked in parser */ >> break; >> + case VERIFYING_EBPF_SIGNATURE: >> + if (pkcs7->data_type != OID_data) { >> + pr_warn("Invalid ebpf sig (not pkcs7-data)\n"); >> + return -EKEYREJECTED; >> + } >> + if (pkcs7->have_authattrs) { >> + pr_warn("Invalid ebpf sig (has authattrs)\n"); >> + return -EKEYREJECTED; >> + } >> + break; >> case VERIFYING_UNSPECIFIED_SIGNATURE: >> if (pkcs7->data_type != OID_data) { >> pr_warn("Invalid unspecified sig (not pkcs7-data)\n"); >> diff --git a/include/linux/kernel_read_file.h >> b/include/linux/kernel_read_file.h >> index 90451e2e12bd1..7ed9337be5423 100644 >> --- a/include/linux/kernel_read_file.h >> +++ b/include/linux/kernel_read_file.h >> @@ -14,6 +14,7 @@ >> id(KEXEC_INITRAMFS, kexec-initramfs) \ >> id(POLICY, security-policy) \ >> id(X509_CERTIFICATE, x509-certificate) \ >> + id(EBPF, ebpf) \ >> id(MAX_ID, ) >> >> #define __fid_enumify(ENUM, dummy) READING_ ## ENUM, >> diff --git a/include/linux/verification.h b/include/linux/verification.h >> index 4f3022d081c31..812be8ad5f744 100644 >> --- a/include/linux/verification.h >> +++ b/include/linux/verification.h >> @@ -35,6 +35,7 @@ enum key_being_used_for { >> VERIFYING_KEXEC_PE_SIGNATURE, >> VERIFYING_KEY_SIGNATURE, >> VERIFYING_KEY_SELF_SIGNATURE, >> + VERIFYING_EBPF_SIGNATURE, >> VERIFYING_UNSPECIFIED_SIGNATURE, >> NR__KEY_BEING_USED_FOR >> }; >> diff --git a/include/uapi/linux/lsm.h b/include/uapi/linux/lsm.h >> index 938593dfd5daf..2ff9bcdd551e2 100644 >> --- a/include/uapi/linux/lsm.h >> +++ b/include/uapi/linux/lsm.h >> @@ -65,6 +65,7 @@ struct lsm_ctx { >> #define LSM_ID_IMA 111 >> #define LSM_ID_EVM 112 >> #define LSM_ID_IPE 113 >> +#define LSM_ID_HORNET 114 >> >> /* >> * LSM_ATTR_XXX definitions identify different LSM attributes >> diff --git a/security/Kconfig b/security/Kconfig >> index f10dbf15c2947..0030f0224c7ab 100644 >> --- a/security/Kconfig >> +++ b/security/Kconfig >> @@ -230,6 +230,7 @@ source "security/safesetid/Kconfig" >> source "security/lockdown/Kconfig" >> source "security/landlock/Kconfig" >> source "security/ipe/Kconfig" >> +source "security/hornet/Kconfig" >> >> source "security/integrity/Kconfig" >> >> @@ -273,7 +274,7 @@ config LSM >> default >> "landlock,lockdown,yama,loadpin,safesetid,apparmor,selinux,smack,tomoyo,ipe,bpf" >> if DEFAULT_SECURITY_APPARMOR >> default "landlock,lockdown,yama,loadpin,safesetid,tomoyo,ipe,bpf" if >> DEFAULT_SECURITY_TOMOYO >> default "landlock,lockdown,yama,loadpin,safesetid,ipe,bpf" if >> DEFAULT_SECURITY_DAC >> - default >> "landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,ipe,bpf" >> + default >> "landlock,lockdown,yama,loadpin,safesetid,selinux,smack,tomoyo,apparmor,ipe,hornet,bpf" >> help >> A comma-separated list of LSMs, in initialization order. >> Any LSMs left off this list, except for those with order >> diff --git a/security/Makefile b/security/Makefile >> index 22ff4c8bd8cec..e24bccd951f88 100644 >> --- a/security/Makefile >> +++ b/security/Makefile >> @@ -26,6 +26,7 @@ obj-$(CONFIG_CGROUPS) += >> device_cgroup.o >> obj-$(CONFIG_BPF_LSM) += bpf/ >> obj-$(CONFIG_SECURITY_LANDLOCK) += landlock/ >> obj-$(CONFIG_SECURITY_IPE) += ipe/ >> +obj-$(CONFIG_SECURITY_HORNET) += hornet/ >> >> # Object integrity file lists >> obj-$(CONFIG_INTEGRITY) += integrity/ >> diff --git a/security/hornet/Kconfig b/security/hornet/Kconfig >> new file mode 100644 >> index 0000000000000..19406aa237ac6 >> --- /dev/null >> +++ b/security/hornet/Kconfig >> @@ -0,0 +1,11 @@ >> +# SPDX-License-Identifier: GPL-2.0-only >> +config SECURITY_HORNET >> + bool "Hornet support" >> + depends on SECURITY >> + default n >> + help >> + This selects Hornet. >> + Further information can be found in >> + Documentation/admin-guide/LSM/Hornet.rst. >> + >> + If you are unsure how to answer this question, answer N. >> diff --git a/security/hornet/Makefile b/security/hornet/Makefile >> new file mode 100644 >> index 0000000000000..79f4657b215fa >> --- /dev/null >> +++ b/security/hornet/Makefile >> @@ -0,0 +1,4 @@ >> +# SPDX-License-Identifier: GPL-2.0-only >> +obj-$(CONFIG_SECURITY_HORNET) := hornet.o >> + >> +hornet-y := hornet_lsm.o >> diff --git a/security/hornet/hornet_lsm.c b/security/hornet/hornet_lsm.c >> new file mode 100644 >> index 0000000000000..3616c68b76fbc >> --- /dev/null >> +++ b/security/hornet/hornet_lsm.c >> @@ -0,0 +1,239 @@ >> +// SPDX-License-Identifier: GPL-2.0-only >> +/* >> + * Hornet Linux Security Module >> + * >> + * Author: Blaise Boscaccy <bbosca...@linux.microsoft.com> >> + * >> + * Copyright (C) 2025 Microsoft Corporation >> + */ >> + >> +#include <linux/lsm_hooks.h> >> +#include <uapi/linux/lsm.h> >> +#include <linux/bpf.h> >> +#include <linux/verification.h> >> +#include <crypto/public_key.h> >> +#include <linux/module_signature.h> >> +#include <crypto/pkcs7.h> >> +#include <linux/bpf_verifier.h> >> +#include <linux/sort.h> >> + >> +#define EBPF_SIG_STRING "~eBPF signature appended~\n" >> + >> +struct hornet_maps { >> + u32 used_idx[MAX_USED_MAPS]; >> + u32 used_map_cnt; >> + bpfptr_t fd_array; >> +}; >> + >> +static int cmp_idx(const void *a, const void *b) >> +{ >> + return *(const u32 *)a - *(const u32 *)b; >> +} >> + >> +static int add_used_map(struct hornet_maps *maps, int idx) >> +{ >> + int i; >> + >> + for (i = 0; i < maps->used_map_cnt; i++) >> + if (maps->used_idx[i] == idx) >> + return i; >> + >> + if (maps->used_map_cnt >= MAX_USED_MAPS) >> + return -E2BIG; >> + >> + maps->used_idx[maps->used_map_cnt] = idx; >> + return maps->used_map_cnt++; >> +} >> + >> +static int hornet_find_maps(struct bpf_prog *prog, struct hornet_maps *maps) >> +{ >> + struct bpf_insn *insn = prog->insnsi; >> + int insn_cnt = prog->len; >> + int i; >> + int err; >> + >> + for (i = 0; i < insn_cnt; i++, insn++) { >> + if (insn[0].code == (BPF_LD | BPF_IMM | BPF_DW)) { >> + switch (insn[0].src_reg) { >> + case BPF_PSEUDO_MAP_IDX_VALUE: >> + case BPF_PSEUDO_MAP_IDX: >> + err = add_used_map(maps, insn[0].imm); >> + if (err < 0) >> + return err; >> + break; >> + default: >> + break; >> + } >> + } >> + } >> + /* Sort the spare-array indices. This should match the map ordering >> used during >> + * signature generation >> + */ >> + sort(maps->used_idx, maps->used_map_cnt, sizeof(*maps->used_idx), >> + cmp_idx, NULL); >> + >> + return 0; >> +} >> + >> +static int hornet_populate_fd_array(struct hornet_maps *maps, u32 >> fd_array_cnt) >> +{ >> + int i; >> + >> + if (fd_array_cnt > MAX_USED_MAPS) >> + return -E2BIG; >> + >> + for (i = 0; i < fd_array_cnt; i++) >> + maps->used_idx[i] = i; >> + >> + maps->used_map_cnt = fd_array_cnt; >> + return 0; >> +} >> + >> +/* kern_sys_bpf is declared as an EXPORT_SYMBOL in kernel/bpf/syscall.c, >> however no definition is >> + * provided in any bpf header files. If/when this function has a proper >> definition provided >> + * somewhere this declaration should be removed >> + */ >> +int kern_sys_bpf(int cmd, union bpf_attr *attr, unsigned int size); >> + >> +static int hornet_verify_lskel(struct bpf_prog *prog, struct hornet_maps >> *maps, >> + void *sig, size_t sig_len) >> +{ >> + int fd; >> + u32 i; >> + void *buf; >> + void *new; >> + size_t buf_sz; >> + struct bpf_map *map; >> + int err = 0; >> + int key = 0; >> + union bpf_attr attr = {0}; >> + >> + buf = kmalloc_array(prog->len, sizeof(struct bpf_insn), GFP_KERNEL); >> + if (!buf) >> + return -ENOMEM; >> + buf_sz = prog->len * sizeof(struct bpf_insn); >> + memcpy(buf, prog->insnsi, buf_sz); >> + >> + for (i = 0; i < maps->used_map_cnt; i++) { >> + err = copy_from_bpfptr_offset(&fd, maps->fd_array, >> + maps->used_idx[i] * sizeof(fd), >> + sizeof(fd)); >> + if (err < 0) >> + continue; >> + if (fd < 1) >> + continue; >> + >> + map = bpf_map_get(fd); >> + if (IS_ERR(map)) >> + continue; >> + >> + /* don't allow userspace to change map data used for signature >> verification */ >> + if (!map->frozen) { >> + attr.map_fd = fd; >> + err = kern_sys_bpf(BPF_MAP_FREEZE, &attr, sizeof(attr)); >> + if (err < 0) >> + goto out; >> + } >> + >> + new = krealloc(buf, buf_sz + map->value_size, GFP_KERNEL); >> + if (!new) { >> + err = -ENOMEM; >> + goto out; >> + } >> + buf = new; >> + new = map->ops->map_lookup_elem(map, &key); >> + if (!new) { >> + err = -ENOENT; >> + goto out; >> + } >> + memcpy(buf + buf_sz, new, map->value_size); >> + buf_sz += map->value_size; >> + } >> + >> + err = verify_pkcs7_signature(buf, buf_sz, sig, sig_len, >> + VERIFY_USE_SECONDARY_KEYRING, >> + VERIFYING_EBPF_SIGNATURE, >> + NULL, NULL); >> +out: >> + kfree(buf); >> + return err; >> +} >> + >> +static int hornet_check_binary(struct bpf_prog *prog, union bpf_attr *attr, >> + struct hornet_maps *maps) >> +{ >> + struct file *file = get_task_exe_file(current); >> + const unsigned long markerlen = sizeof(EBPF_SIG_STRING) - 1; >> + void *buf = NULL; >> + size_t sz = 0, sig_len, prog_len, buf_sz; >> + int err = 0; >> + struct module_signature sig; >> + >> + buf_sz = kernel_read_file(file, 0, &buf, INT_MAX, &sz, READING_EBPF); >> + fput(file); >> + if (!buf_sz) >> + return -1; >> + >> + prog_len = buf_sz; >> + >> + if (prog_len > markerlen && >> + memcmp(buf + prog_len - markerlen, EBPF_SIG_STRING, markerlen) == 0) >> + prog_len -= markerlen; >> + >> + memcpy(&sig, buf + (prog_len - sizeof(sig)), sizeof(sig)); >> + sig_len = be32_to_cpu(sig.sig_len); >> + prog_len -= sig_len + sizeof(sig); >> + >> + err = mod_check_sig(&sig, prog->len * sizeof(struct bpf_insn), "ebpf"); >> + if (err) >> + return err; >> + return hornet_verify_lskel(prog, maps, buf + prog_len, sig_len); >> +} >> + >> +static int hornet_check_signature(struct bpf_prog *prog, union bpf_attr >> *attr, >> + struct bpf_token *token, bool is_kernel) > > It's a little confusing that you are passing is_kernel in here, when the > only caller will always pass in true. Is there a good reason not to > drop the arg here and pass 'true' in to make_bpfptr(). Of course, then > people will ask why not define an IS_KERNEL to true as passing true to > second argument is cryptic... Maybe you just can't win here :) >
Initially during development churn, this code was using a bpfptr_t that ended up becoming a boolean flag in the LSM hooks and this appears to be a relic of that. I think I'll remove the boolean param to hornet_check_signature since this code is only interested in checking stuff that originiated in userspace. >> +{ >> + struct hornet_maps maps = {0}; >> + int err; >> + >> + /* support both sparse arrays and explicit continuous arrays of map fds >> */ >> + if (attr->fd_array_cnt) >> + err = hornet_populate_fd_array(&maps, attr->fd_array_cnt); >> + else >> + err = hornet_find_maps(prog, &maps); >> + >> + if (err < 0) >> + return err; >> + >> + maps.fd_array = make_bpfptr(attr->fd_array, is_kernel); >> + return hornet_check_binary(prog, attr, &maps); >> +} >> + >> +static int hornet_bpf_prog_load(struct bpf_prog *prog, union bpf_attr *attr, >> + struct bpf_token *token, bool is_kernel) >> +{ >> + if (is_kernel) >> + return 0; >> + return hornet_check_signature(prog, attr, token, is_kernel); >> +} >> + >> +static struct security_hook_list hornet_hooks[] __ro_after_init = { >> + LSM_HOOK_INIT(bpf_prog_load, hornet_bpf_prog_load), >> +}; >> + >> +static const struct lsm_id hornet_lsmid = { >> + .name = "hornet", >> + .id = LSM_ID_HORNET, >> +}; >> + >> +static int __init hornet_init(void) >> +{ >> + pr_info("Hornet: eBPF signature verification enabled\n"); >> + security_add_hooks(hornet_hooks, ARRAY_SIZE(hornet_hooks), >> &hornet_lsmid); >> + return 0; >> +} >> + >> +DEFINE_LSM(hornet) = { >> + .name = "hornet", >> + .init = hornet_init, >> +}; >> -- >> 2.48.1 >>