[PATCH v5 06/10] security: Hornet LSM

Blaise Boscaccy bboscaccy at linux.microsoft.com
Wed Apr 29 18:34:55 UTC 2026


Fan Wu <wufan at kernel.org> writes:

> 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?
>

gah thanks. 
>> +--
>> +-- 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.
>

Yes, it can. We can support that.

>> +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.
>

It turns out that we can. I'll send out the simplified version in v6.

> -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