From: Blaise Boscaccy <bboscaccy@linux.microsoft.com>
To: Fan Wu <wufan@kernel.org>
Cc: "Jonathan Corbet" <corbet@lwn.net>,
"Paul Moore" <paul@paul-moore.com>,
"James Morris" <jmorris@namei.org>,
"Serge E. Hallyn" <serge@hallyn.com>,
"Mickaël Salaün" <mic@digikod.net>,
"Günther Noack" <gnoack@google.com>,
"Dr. David Alan Gilbert" <linux@treblig.org>,
"Andrew Morton" <akpm@linux-foundation.org>,
James.Bottomley@hansenpartnership.com, dhowells@redhat.com,
"Fan Wu" <wufan@kernel.org>,
"Ryan Foster" <foster.ryan.r@gmail.com>,
"Randy Dunlap" <rdunlap@infradead.org>,
linux-security-module@vger.kernel.org, linux-doc@vger.kernel.org,
linux-kernel@vger.kernel.org, bpf@vger.kernel.org,
"Song Liu" <song@kernel.org>
Subject: Re: [PATCH v5 06/10] security: Hornet LSM
Date: Wed, 29 Apr 2026 11:34:55 -0700 [thread overview]
Message-ID: <87jytpk434.fsf@microsoft.com> (raw)
In-Reply-To: <CAKtyLkF5L_sDHmtT2eVhmHLdhB_rAxWzotUJER6A5DFQ=S6EPw@mail.gmail.com>
Fan Wu <wufan@kernel.org> writes:
> On Mon, Apr 20, 2026 at 2:27 PM Blaise Boscaccy
> <bboscaccy@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@linux.microsoft.com>
>> Nacked-by: Alexei Starovoitov <alexei.starovoitov@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@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
>>
next prev parent reply other threads:[~2026-04-29 18:34 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-20 21:26 [PATCH v5 00/10] Reintroduce Hornet LSM Blaise Boscaccy
2026-04-20 21:26 ` [PATCH v5 01/10] crypto: pkcs7: add flag for validated trust on a signed info block Blaise Boscaccy
2026-04-20 21:26 ` [PATCH v5 02/10] crypto: pkcs7: add ability to extract signed attributes by OID Blaise Boscaccy
2026-04-20 21:26 ` [PATCH v5 03/10] crypto: pkcs7: add tests for pkcs7_get_authattr Blaise Boscaccy
2026-04-20 21:26 ` [PATCH v5 04/10] lsm: framework for BPF integrity verification Blaise Boscaccy
2026-04-20 21:26 ` [PATCH v5 05/10] lsm: security: Add additional enum values for bpf integrity checks Blaise Boscaccy
2026-04-20 21:26 ` [PATCH v5 06/10] security: Hornet LSM Blaise Boscaccy
2026-04-21 0:08 ` Fan Wu
2026-04-29 18:34 ` Blaise Boscaccy [this message]
2026-04-23 18:37 ` [PATCH v5 6/10] " Paul Moore
2026-04-20 21:26 ` [PATCH v5 07/10] hornet: Introduce gen_sig Blaise Boscaccy
2026-04-21 0:18 ` Fan Wu
2026-04-20 21:26 ` [PATCH v5 08/10] hornet: Add a light skeleton data extractor scripts Blaise Boscaccy
2026-04-20 21:26 ` [PATCH v5 09/10] selftests/hornet: Add a selftest for the Hornet LSM Blaise Boscaccy
2026-04-20 21:26 ` [PATCH v5 10/10] ipe: Add BPF program load policy enforcement via Hornet integration Blaise Boscaccy
2026-04-21 0:27 ` Fan Wu
2026-04-29 18:35 ` Blaise Boscaccy
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=87jytpk434.fsf@microsoft.com \
--to=bboscaccy@linux.microsoft.com \
--cc=James.Bottomley@hansenpartnership.com \
--cc=akpm@linux-foundation.org \
--cc=bpf@vger.kernel.org \
--cc=corbet@lwn.net \
--cc=dhowells@redhat.com \
--cc=foster.ryan.r@gmail.com \
--cc=gnoack@google.com \
--cc=jmorris@namei.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-security-module@vger.kernel.org \
--cc=linux@treblig.org \
--cc=mic@digikod.net \
--cc=paul@paul-moore.com \
--cc=rdunlap@infradead.org \
--cc=serge@hallyn.com \
--cc=song@kernel.org \
--cc=wufan@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox