Message ID | 20250321164537.16719-2-bboscaccy@linux.microsoft.com (mailing list archive) |
---|---|
State | RFC |
Headers | show |
Series | Introducing Hornet LSM | expand |
Context | Check | Description |
---|---|---|
netdev/tree_selection | success | Not a local patch, async |
Blaise Boscaccy <bboscaccy@linux.microsoft.com> writes: > 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 > 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 > programs which where loaded from inside kernel rather than userspace, > 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@linux.microsoft.com> > --- > Documentation/admin-guide/LSM/Hornet.rst | 51 +++++ You will need to add that file to .../index.rst, or it won't be included in the docs build. Thanks, jon
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@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 <bboscaccy@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 :) > +{ > + 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 >
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@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) +{ + 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, +};
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 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 programs which where loaded from inside kernel rather than userspace, 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@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