[PATCH v5 06/10] security: Hornet LSM

Fan Wu wufan at kernel.org
Tue Apr 21 00:08:42 UTC 2026


On Mon, Apr 20, 2026 at 2:27 PM Blaise Boscaccy
<bboscaccy at linux.microsoft.com> wrote:
>
> This adds the Hornet Linux Security Module which provides enhanced
> signature verification and data validation for eBPF programs. This
> allows users to continue to maintain an invariant that all code
> running inside of the kernel has actually been signed and verified, by
> the kernel.
>
> This effort builds upon the currently excepted upstream solution. It
> further hardens it by providing deterministic, in-kernel checking of
> map hashes to solidify auditing along with preventing TOCTOU attacks
> against lskel map hashes.
>
> Target map hashes are passed in via PKCS#7 signed attributes. Hornet
> determines the extent which the eBFP program is signed and defers to
> other LSMs for policy decisions.
>
> Signed-off-by: Blaise Boscaccy <bboscaccy at linux.microsoft.com>
> Nacked-by: Alexei Starovoitov <alexei.starovoitov at gmail.com>

...

> 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

The dependency doesn't seem to be complete, for example,
PKCS7_MESSAGE_PARSER is not selected.

> +       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..26b6f954f762e
> --- /dev/null
> +++ b/security/hornet/Makefile
> @@ -0,0 +1,7 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +obj-$(CONFIG_SECURITY_HORNET) := hornet.o
> +
> +hornet-y := hornet.asn1.o \
> +       hornet_lsm.o \
> +
> +$(obj)/hornet.asn1.o: $(obj)/hornet.asn1.c $(obj)/hornet.asn1.h
> diff --git a/security/hornet/hornet.asn1 b/security/hornet/hornet.asn1
> new file mode 100644
> index 0000000000000..c8d47b16b65d7
> --- /dev/null
> +++ b/security/hornet/hornet.asn1
> @@ -0,0 +1,13 @@
> +-- SPDX-License-Identifier: BSD-3-Clause
> +--
> +-- Copyright (C) 2009 IETF Trust and the persons identified as authors
> +-- of the code

I'm not a lawyer, but since this is a new AA which is not in the RFC,
should the copyright belong to IETF?

> +--
> +-- https://www.rfc-editor.org/rfc/rfc5652#section-3
> +
> +HornetData ::= SET OF Map
> +
> +Map ::= SEQUENCE {
> +       index                   INTEGER ({ hornet_map_index }),
> +       sha                     OCTET STRING ({ hornet_map_hash })
> +} ({ hornet_next_map })
> diff --git a/security/hornet/hornet_lsm.c b/security/hornet/hornet_lsm.c
> new file mode 100644
> index 0000000000000..f7d62fe6229c9
> --- /dev/null
> +++ b/security/hornet/hornet_lsm.c
> @@ -0,0 +1,346 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Hornet Linux Security Module
> + *
> + * Author: Blaise Boscaccy <bboscaccy at linux.microsoft.com>
> + *
> + * Copyright (C) 2026 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/sort.h>
> +#include <linux/asn1_decoder.h>
> +#include <linux/oid_registry.h>
> +#include "hornet.asn1.h"
> +
> +#define MAX_USED_MAPS 64
> +
> +struct hornet_maps {
> +       bpfptr_t fd_array;
> +};
> +
> +/* The only hashing algorithm available is SHA256 due to it be hardcoded
> +   in the bpf subsystem. */
> +
> +struct hornet_parse_context {
> +       int indexes[MAX_USED_MAPS];
> +       bool skips[MAX_USED_MAPS];
> +       unsigned char hashes[SHA256_DIGEST_SIZE * MAX_USED_MAPS];
> +       int hash_count;
> +};
> +
> +struct hornet_prog_security_struct {
> +       bool checked[MAX_USED_MAPS];
> +       unsigned char hashes[SHA256_DIGEST_SIZE * MAX_USED_MAPS];
> +};
> +
> +struct hornet_map_security_struct {
> +       bool checked;
> +       int index;
> +};
> +

Can maps be shared between programs? If so, since the LSM blob is
per-map, a second program's load will overwrite map_security->index
set by the first. A later run of the first program would then fail to
find its own map.

> +struct lsm_blob_sizes hornet_blob_sizes __ro_after_init = {
> +       .lbs_bpf_map = sizeof(struct hornet_map_security_struct),
> +       .lbs_bpf_prog = sizeof(struct hornet_prog_security_struct),
> +};
> +
> +static inline struct hornet_prog_security_struct *
> +hornet_bpf_prog_security(struct bpf_prog *prog)
> +{
> +       return prog->aux->security + hornet_blob_sizes.lbs_bpf_prog;
> +}
> +
> +static inline struct hornet_map_security_struct *
> +hornet_bpf_map_security(struct bpf_map *map)
> +{
> +       return map->security + hornet_blob_sizes.lbs_bpf_map;
> +}
> +
> +static int hornet_verify_hashes(struct hornet_maps *maps,
> +                               struct hornet_parse_context *ctx,
> +                               struct bpf_prog *prog)
> +{
> +       int map_fd;
> +       u32 i;
> +       struct bpf_map *map;
> +       int err = 0;
> +       unsigned char hash[SHA256_DIGEST_SIZE];
> +       struct hornet_prog_security_struct *security = hornet_bpf_prog_security(prog);
> +       struct hornet_map_security_struct *map_security;
> +
> +       for (i = 0; i < ctx->hash_count; i++) {
> +               if (ctx->skips[i])
> +                       continue;
> +
> +               err = copy_from_bpfptr_offset(&map_fd, maps->fd_array,
> +                                             ctx->indexes[i] * sizeof(map_fd),
> +                                             sizeof(map_fd));
> +               if (err < 0)
> +                       return LSM_INT_VERDICT_FAULT;
> +
> +               CLASS(fd, f)(map_fd);
> +               if (fd_empty(f))
> +                       return LSM_INT_VERDICT_FAULT;
> +               if (unlikely(fd_file(f)->f_op != &bpf_map_fops))
> +                       return LSM_INT_VERDICT_FAULT;
> +
> +               map = fd_file(f)->private_data;
> +               if (!map->frozen)
> +                       return LSM_INT_VERDICT_FAULT;
> +
> +               map->ops->map_get_hash(map, SHA256_DIGEST_SIZE, hash);
> +
> +               err = memcmp(hash, &ctx->hashes[i * SHA256_DIGEST_SIZE],
> +                             SHA256_DIGEST_SIZE);
> +               if (err)
> +                       return LSM_INT_VERDICT_UNEXPECTED;
> +
> +               security->checked[i] = true;
> +               memcpy(&security->hashes[i * SHA256_DIGEST_SIZE], hash, SHA256_DIGEST_SIZE);
> +               map_security = hornet_bpf_map_security(map);
> +               map_security->checked = true;
> +               map_security->index = i;
> +       }
> +       return LSM_INT_VERDICT_OK;
> +}
> +
> +int hornet_next_map(void *context, size_t hdrlen,
> +                    unsigned char tag,
> +                    const void *value, size_t vlen)
> +{
> +       struct hornet_parse_context *ctx = (struct hornet_parse_context *)context;
> +
> +       if (++ctx->hash_count >= MAX_USED_MAPS)
> +               return -EINVAL;
> +       return 0;
> +}
> +
> +int hornet_map_index(void *context, size_t hdrlen,
> +                    unsigned char tag,
> +                    const void *value, size_t vlen)
> +{
> +       struct hornet_parse_context *ctx = (struct hornet_parse_context *)context;
> +
> +       if (vlen > 1)
> +               return -EINVAL;
> +
> +       ctx->indexes[ctx->hash_count] = *(u8 *)value;
> +       return 0;
> +}
> +
> +int hornet_map_hash(void *context, size_t hdrlen,
> +                   unsigned char tag,
> +                   const void *value, size_t vlen)
> +
> +{
> +       struct hornet_parse_context *ctx = (struct hornet_parse_context *)context;
> +
> +       if (vlen != SHA256_DIGEST_SIZE && vlen != 0)
> +               return -EINVAL;
> +
> +       if (vlen) {
> +               ctx->skips[ctx->hash_count] = false;
> +               memcpy(&ctx->hashes[ctx->hash_count * SHA256_DIGEST_SIZE], value, vlen);
> +       } else
> +               ctx->skips[ctx->hash_count] = true;
> +
> +       return 0;
> +}
> +
> +static int hornet_check_program(struct bpf_prog *prog, union bpf_attr *attr,
> +                               struct bpf_token *token, bool is_kernel,
> +                               enum lsm_integrity_verdict *verdict)
> +{
> +       struct hornet_maps maps = {0};
> +       bpfptr_t usig = make_bpfptr(attr->signature, is_kernel);
> +       struct pkcs7_message *msg;
> +       struct hornet_parse_context *ctx;
> +       void *sig;
> +       int err;
> +       const void *authattrs;
> +       size_t authattrs_len;
> +       struct key *key;
> +
> +       if (!attr->signature) {
> +               *verdict = LSM_INT_VERDICT_UNSIGNED;
> +               return 0;
> +       }
> +
> +       ctx = kzalloc(sizeof(struct hornet_parse_context), GFP_KERNEL);
> +       if (!ctx)
> +               return -ENOMEM;
> +
> +       maps.fd_array = make_bpfptr(attr->fd_array, is_kernel);
> +       sig = kzalloc(attr->signature_size, GFP_KERNEL);
> +       if (!sig) {
> +               err = -ENOMEM;
> +               goto out;
> +       }
> +       err = copy_from_bpfptr(sig, usig, attr->signature_size);
> +       if (err != 0)
> +               goto cleanup_sig;
> +
> +       msg = pkcs7_parse_message(sig, attr->signature_size);
> +       if (IS_ERR(msg)) {
> +               *verdict = LSM_INT_VERDICT_BADSIG;
> +               err = 0;
> +               goto cleanup_sig;
> +       }
> +
> +       if (system_keyring_id_check(attr->keyring_id) == 0)
> +               key = (struct key*)(unsigned long)attr->keyring_id;
> +       else
> +               key = key_ref_to_ptr(lookup_user_key(attr->keyring_id, 0, KEY_DEFER_PERM_CHECK));

You might need to key_put the user key.

> +
> +       if (verify_pkcs7_message_sig(prog->insnsi, prog->len * sizeof(struct bpf_insn), msg,
> +                                    key,
> +                                    VERIFYING_BPF_SIGNATURE,
> +                                    NULL, NULL)) {
> +               *verdict = LSM_INT_VERDICT_UNKNOWNKEY;
> +               err = 0;
> +               goto cleanup_msg;
> +       }
> +
> +       if (pkcs7_get_authattr(msg, OID_hornet_data,
> +                              &authattrs, &authattrs_len) == -ENODATA) {
> +               *verdict = LSM_INT_VERDICT_PARTIALSIG;
> +               err = 0;
> +               goto cleanup_msg;
> +       }
> +
> +       err = asn1_ber_decoder(&hornet_decoder, ctx, authattrs, authattrs_len);
> +       if (err < 0 || authattrs == NULL) {
> +               *verdict = LSM_INT_VERDICT_BADSIG;
> +               err = 0;
> +               goto cleanup_msg;
> +       }
> +
> +       err = hornet_verify_hashes(&maps, ctx, prog);
> +       if (err == 0)
> +               *verdict = LSM_INT_VERDICT_OK;
> +       else
> +               *verdict = err;
> +
> +cleanup_msg:
> +       pkcs7_free_message(msg);
> +cleanup_sig:
> +       kfree(sig);
> +out:
> +       kfree(ctx);
> +       return err;
> +}
> +
> +static const struct lsm_id hornet_lsmid = {
> +       .name = "hornet",
> +       .id = LSM_ID_HORNET,
> +};
> +
> +static int hornet_bpf_prog_load_integrity(struct bpf_prog *prog, union bpf_attr *attr,
> +                                         struct bpf_token *token, bool is_kernel)
> +{
> +       enum lsm_integrity_verdict verdict;
> +       int result = hornet_check_program(prog, attr, token, is_kernel, &verdict);
> +
> +       if (result < 0)
> +               return result;
> +
> +       return security_bpf_prog_load_post_integrity(prog, attr, token, is_kernel,
> +                                                    &hornet_lsmid, verdict);
> +}
> +
> +static int hornet_verify_map(struct bpf_prog *prog, int index)
> +{
> +       unsigned char hash[SHA256_DIGEST_SIZE];
> +       int i;
> +       struct bpf_map *map;
> +       struct hornet_prog_security_struct *security = hornet_bpf_prog_security(prog);
> +       struct hornet_map_security_struct *map_security;
> +
> +       if (!security->checked[index])
> +               return 0;
> +
> +       for (i = 0; i < prog->aux->used_map_cnt; i++) {
> +               map = prog->aux->used_maps[i];
> +               map_security = hornet_bpf_map_security(map);
> +               if (map_security->index != index)
> +                       continue;
> +
> +               if (!map->frozen)
> +                       return -EPERM;
> +
> +               map->ops->map_get_hash(map, SHA256_DIGEST_SIZE, hash);
> +               if (memcmp(hash, &security->hashes[index * SHA256_DIGEST_SIZE],
> +                          SHA256_DIGEST_SIZE) != 0)
> +                       return -EPERM;
> +               else
> +                       return 0;
> +       }
> +       return -EINVAL;
> +}
> +
> +static int hornet_check_prog_maps(u32 ufd)
> +{
> +       CLASS(fd, f)(ufd);
> +       struct bpf_prog *prog;
> +       int i, result = 0;
> +
> +       if (fd_empty(f))
> +               return -EBADF;
> +       if (fd_file(f)->f_op != &bpf_prog_fops)
> +               return -EINVAL;
> +
> +       prog = fd_file(f)->private_data;
> +
> +       mutex_lock(&prog->aux->used_maps_mutex);
> +       if (!prog->aux->used_map_cnt)
> +               goto out;
> +
> +       for (i = 0; i < prog->aux->used_map_cnt; i++) {
> +               result = hornet_verify_map(prog, i);
> +               if (result)
> +                       goto out;
> +       }

This loop is kind of confusing for me, I guess it's just iterating
through the maps the program currently in use. I feel the nested loop
is unnecessary and the hash could be saved into the map's blob
instead. Please correct me if I'm wrong here.

-Fan

> +out:
> +       mutex_unlock(&prog->aux->used_maps_mutex);
> +       return result;
> +}
> +
> +static int hornet_bpf(int cmd, union bpf_attr *attr, unsigned int size, bool kernel)
> +{
> +       /* in horent_bpf(), anything that had originated from kernel space we assume
> +          has already been checked, in some form or another, so we don't bother
> +          checking the intergity of any maps. In hornet_bpf_prog_load_integrity(),
> +          hornet doesn't make any opinion on that and delegates that to the downstream
> +          policy enforcement. */
> +
> +       if (cmd != BPF_PROG_RUN)
> +               return 0;
> +       if (kernel)
> +               return 0;
> +
> +       return hornet_check_prog_maps(attr->test.prog_fd);
> +}
> +
> +static struct security_hook_list hornet_hooks[] __ro_after_init = {
> +       LSM_HOOK_INIT(bpf_prog_load_integrity, hornet_bpf_prog_load_integrity),
> +       LSM_HOOK_INIT(bpf, hornet_bpf),
> +};
> +
> +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) = {
> +       .id = &hornet_lsmid,
> +       .blobs = &hornet_blob_sizes,
> +       .init = hornet_init,
> +};
> --
> 2.53.0
>



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