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 <bboscaccy@xxxxxxxxxxxxxxxxxxx> > --- > 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 <bboscaccy@xxxxxxxxxxxxxxxxxxx> > + * > + * 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 :) > +{ > + 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 >