[RFC PATCH security-next 1/4] security: Hornet LSM

Blaise Boscaccy
2025-03-21T16:46:08Z

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

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,
+};
--
2.48.1